123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- /* Copyright 2012 Mozilla Foundation
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import { createPromiseCapability, PDFDateString } from "pdfjs-lib";
- import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
- const DEFAULT_FIELD_CONTENT = "-";
- // See https://en.wikibooks.org/wiki/Lentis/Conversion_to_the_Metric_Standard_in_the_United_States
- const NON_METRIC_LOCALES = ["en-us", "en-lr", "my"];
- // Should use the format: `width x height`, in portrait orientation.
- // See https://en.wikipedia.org/wiki/Paper_size
- const US_PAGE_NAMES = {
- "8.5x11": "Letter",
- "8.5x14": "Legal",
- };
- const METRIC_PAGE_NAMES = {
- "297x420": "A3",
- "210x297": "A4",
- };
- function getPageName(size, isPortrait, pageNames) {
- const width = isPortrait ? size.width : size.height;
- const height = isPortrait ? size.height : size.width;
- return pageNames[`${width}x${height}`];
- }
- /**
- * @typedef {Object} PDFDocumentPropertiesOptions
- * @property {HTMLDialogElement} dialog - The overlay's DOM element.
- * @property {Object} fields - Names and elements of the overlay's fields.
- * @property {HTMLButtonElement} closeButton - Button for closing the overlay.
- */
- class PDFDocumentProperties {
- #fieldData = null;
- /**
- * @param {PDFDocumentPropertiesOptions} options
- * @param {OverlayManager} overlayManager - Manager for the viewer overlays.
- * @param {EventBus} eventBus - The application event bus.
- * @param {IL10n} l10n - Localization service.
- * @param {function} fileNameLookup - The function that is used to lookup
- * the document fileName.
- */
- constructor(
- { dialog, fields, closeButton },
- overlayManager,
- eventBus,
- l10n,
- fileNameLookup
- ) {
- this.dialog = dialog;
- this.fields = fields;
- this.overlayManager = overlayManager;
- this.l10n = l10n;
- this._fileNameLookup = fileNameLookup;
- this.#reset();
- // Bind the event listener for the Close button.
- closeButton.addEventListener("click", this.close.bind(this));
- this.overlayManager.register(this.dialog);
- eventBus._on("pagechanging", evt => {
- this._currentPageNumber = evt.pageNumber;
- });
- eventBus._on("rotationchanging", evt => {
- this._pagesRotation = evt.pagesRotation;
- });
- this._isNonMetricLocale = true; // The default viewer locale is 'en-us'.
- l10n.getLanguage().then(locale => {
- this._isNonMetricLocale = NON_METRIC_LOCALES.includes(locale);
- });
- }
- /**
- * Open the document properties overlay.
- */
- async open() {
- await Promise.all([
- this.overlayManager.open(this.dialog),
- this._dataAvailableCapability.promise,
- ]);
- const currentPageNumber = this._currentPageNumber;
- const pagesRotation = this._pagesRotation;
- // If the document properties were previously fetched (for this PDF file),
- // just update the dialog immediately to avoid redundant lookups.
- if (
- this.#fieldData &&
- currentPageNumber === this.#fieldData._currentPageNumber &&
- pagesRotation === this.#fieldData._pagesRotation
- ) {
- this.#updateUI();
- return;
- }
- // Get the document properties.
- const {
- info,
- /* metadata, */
- /* contentDispositionFilename, */
- contentLength,
- } = await this.pdfDocument.getMetadata();
- const [
- fileName,
- fileSize,
- creationDate,
- modificationDate,
- pageSize,
- isLinearized,
- ] = await Promise.all([
- this._fileNameLookup(),
- this.#parseFileSize(contentLength),
- this.#parseDate(info.CreationDate),
- this.#parseDate(info.ModDate),
- this.pdfDocument.getPage(currentPageNumber).then(pdfPage => {
- return this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation);
- }),
- this.#parseLinearization(info.IsLinearized),
- ]);
- this.#fieldData = Object.freeze({
- fileName,
- fileSize,
- title: info.Title,
- author: info.Author,
- subject: info.Subject,
- keywords: info.Keywords,
- creationDate,
- modificationDate,
- creator: info.Creator,
- producer: info.Producer,
- version: info.PDFFormatVersion,
- pageCount: this.pdfDocument.numPages,
- pageSize,
- linearized: isLinearized,
- _currentPageNumber: currentPageNumber,
- _pagesRotation: pagesRotation,
- });
- this.#updateUI();
- // Get the correct fileSize, since it may not have been available
- // or could potentially be wrong.
- const { length } = await this.pdfDocument.getDownloadInfo();
- if (contentLength === length) {
- return; // The fileSize has already been correctly set.
- }
- const data = Object.assign(Object.create(null), this.#fieldData);
- data.fileSize = await this.#parseFileSize(length);
- this.#fieldData = Object.freeze(data);
- this.#updateUI();
- }
- /**
- * Close the document properties overlay.
- */
- async close() {
- this.overlayManager.close(this.dialog);
- }
- /**
- * Set a reference to the PDF document in order to populate the dialog fields
- * with the document properties. Note that the dialog will contain no
- * information if this method is not called.
- *
- * @param {PDFDocumentProxy} pdfDocument - A reference to the PDF document.
- */
- setDocument(pdfDocument) {
- if (this.pdfDocument) {
- this.#reset();
- this.#updateUI(true);
- }
- if (!pdfDocument) {
- return;
- }
- this.pdfDocument = pdfDocument;
- this._dataAvailableCapability.resolve();
- }
- #reset() {
- this.pdfDocument = null;
- this.#fieldData = null;
- this._dataAvailableCapability = createPromiseCapability();
- this._currentPageNumber = 1;
- this._pagesRotation = 0;
- }
- /**
- * Always updates all of the dialog fields, to prevent inconsistent UI state.
- * NOTE: If the contents of a particular field is neither a non-empty string,
- * nor a number, it will fall back to `DEFAULT_FIELD_CONTENT`.
- */
- #updateUI(reset = false) {
- if (reset || !this.#fieldData) {
- for (const id in this.fields) {
- this.fields[id].textContent = DEFAULT_FIELD_CONTENT;
- }
- return;
- }
- if (this.overlayManager.active !== this.dialog) {
- // Don't bother updating the dialog if has already been closed,
- // since it will be updated the next time `this.open` is called.
- return;
- }
- for (const id in this.fields) {
- const content = this.#fieldData[id];
- this.fields[id].textContent =
- content || content === 0 ? content : DEFAULT_FIELD_CONTENT;
- }
- }
- async #parseFileSize(fileSize = 0) {
- const kb = fileSize / 1024,
- mb = kb / 1024;
- if (!kb) {
- return undefined;
- }
- return this.l10n.get(`document_properties_${mb >= 1 ? "mb" : "kb"}`, {
- size_mb: mb >= 1 && (+mb.toPrecision(3)).toLocaleString(),
- size_kb: mb < 1 && (+kb.toPrecision(3)).toLocaleString(),
- size_b: fileSize.toLocaleString(),
- });
- }
- async #parsePageSize(pageSizeInches, pagesRotation) {
- if (!pageSizeInches) {
- return undefined;
- }
- // Take the viewer rotation into account as well; compare with Adobe Reader.
- if (pagesRotation % 180 !== 0) {
- pageSizeInches = {
- width: pageSizeInches.height,
- height: pageSizeInches.width,
- };
- }
- const isPortrait = isPortraitOrientation(pageSizeInches);
- let sizeInches = {
- width: Math.round(pageSizeInches.width * 100) / 100,
- height: Math.round(pageSizeInches.height * 100) / 100,
- };
- // 1in == 25.4mm; no need to round to 2 decimals for millimeters.
- let sizeMillimeters = {
- width: Math.round(pageSizeInches.width * 25.4 * 10) / 10,
- height: Math.round(pageSizeInches.height * 25.4 * 10) / 10,
- };
- let rawName =
- getPageName(sizeInches, isPortrait, US_PAGE_NAMES) ||
- getPageName(sizeMillimeters, isPortrait, METRIC_PAGE_NAMES);
- if (
- !rawName &&
- !(
- Number.isInteger(sizeMillimeters.width) &&
- Number.isInteger(sizeMillimeters.height)
- )
- ) {
- // Attempt to improve the page name detection by falling back to fuzzy
- // matching of the metric dimensions, to account for e.g. rounding errors
- // and/or PDF files that define the page sizes in an imprecise manner.
- const exactMillimeters = {
- width: pageSizeInches.width * 25.4,
- height: pageSizeInches.height * 25.4,
- };
- const intMillimeters = {
- width: Math.round(sizeMillimeters.width),
- height: Math.round(sizeMillimeters.height),
- };
- // Try to avoid false positives, by only considering "small" differences.
- if (
- Math.abs(exactMillimeters.width - intMillimeters.width) < 0.1 &&
- Math.abs(exactMillimeters.height - intMillimeters.height) < 0.1
- ) {
- rawName = getPageName(intMillimeters, isPortrait, METRIC_PAGE_NAMES);
- if (rawName) {
- // Update *both* sizes, computed above, to ensure that the displayed
- // dimensions always correspond to the detected page name.
- sizeInches = {
- width: Math.round((intMillimeters.width / 25.4) * 100) / 100,
- height: Math.round((intMillimeters.height / 25.4) * 100) / 100,
- };
- sizeMillimeters = intMillimeters;
- }
- }
- }
- const [{ width, height }, unit, name, orientation] = await Promise.all([
- this._isNonMetricLocale ? sizeInches : sizeMillimeters,
- this.l10n.get(
- `document_properties_page_size_unit_${
- this._isNonMetricLocale ? "inches" : "millimeters"
- }`
- ),
- rawName &&
- this.l10n.get(
- `document_properties_page_size_name_${rawName.toLowerCase()}`
- ),
- this.l10n.get(
- `document_properties_page_size_orientation_${
- isPortrait ? "portrait" : "landscape"
- }`
- ),
- ]);
- return this.l10n.get(
- `document_properties_page_size_dimension_${name ? "name_" : ""}string`,
- {
- width: width.toLocaleString(),
- height: height.toLocaleString(),
- unit,
- name,
- orientation,
- }
- );
- }
- async #parseDate(inputDate) {
- const dateObject = PDFDateString.toDateObject(inputDate);
- if (!dateObject) {
- return undefined;
- }
- return this.l10n.get("document_properties_date_string", {
- date: dateObject.toLocaleDateString(),
- time: dateObject.toLocaleTimeString(),
- });
- }
- #parseLinearization(isLinearized) {
- return this.l10n.get(
- `document_properties_linearized_${isLinearized ? "yes" : "no"}`
- );
- }
- }
- export { PDFDocumentProperties };
|