fork from https://github.com/mozilla/pdf.js.git
This commit is contained in:
371
web/pdf_document_properties.js
Normal file
371
web/pdf_document_properties.js
Normal file
@@ -0,0 +1,371 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/** @typedef {import("./event_utils.js").EventBus} EventBus */
|
||||
/** @typedef {import("./overlay_manager.js").OverlayManager} OverlayManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
|
||||
|
||||
import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
|
||||
import { internalOpt } from "./internal_evt.js";
|
||||
import { PDFDateString } from "pdfjs-lib";
|
||||
|
||||
// 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. The names,
|
||||
// which are l10n-ids, should be lowercase.
|
||||
// See https://en.wikipedia.org/wiki/Paper_size
|
||||
const US_PAGE_NAMES = {
|
||||
"8.5x11": "pdfjs-document-properties-page-size-name-letter",
|
||||
"8.5x14": "pdfjs-document-properties-page-size-name-legal",
|
||||
};
|
||||
const METRIC_PAGE_NAMES = {
|
||||
"297x420": "pdfjs-document-properties-page-size-name-a-three",
|
||||
"210x297": "pdfjs-document-properties-page-size-name-a-four",
|
||||
};
|
||||
|
||||
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 {L10n} l10n - Localization service.
|
||||
* @param {function} fileNameLookup - The function that is used to lookup
|
||||
* the document fileName.
|
||||
*/
|
||||
constructor(
|
||||
{ dialog, fields, closeButton },
|
||||
overlayManager,
|
||||
eventBus,
|
||||
l10n,
|
||||
fileNameLookup,
|
||||
titleLookup
|
||||
) {
|
||||
this.dialog = dialog;
|
||||
this.fields = fields;
|
||||
this.overlayManager = overlayManager;
|
||||
this.l10n = l10n;
|
||||
this._fileNameLookup = fileNameLookup;
|
||||
this._titleLookup = titleLookup;
|
||||
|
||||
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;
|
||||
},
|
||||
internalOpt
|
||||
);
|
||||
eventBus.on(
|
||||
"rotationchanging",
|
||||
evt => {
|
||||
this._pagesRotation = evt.pagesRotation;
|
||||
},
|
||||
internalOpt
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
this._fieldDataLastUpdated = Date.now();
|
||||
}
|
||||
|
||||
// Get the document properties.
|
||||
const [
|
||||
{ info, metadata, /* contentDispositionFilename, */ contentLength },
|
||||
pdfPage,
|
||||
] = await Promise.all([
|
||||
this.pdfDocument.getMetadata(),
|
||||
this.pdfDocument.getPage(currentPageNumber).catch(reason => {
|
||||
console.error(
|
||||
`PDFDocumentProperties - unable to get page ${currentPageNumber}.`,
|
||||
reason
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
]);
|
||||
|
||||
const [
|
||||
fileName,
|
||||
fileSize,
|
||||
title,
|
||||
creationDate,
|
||||
modificationDate,
|
||||
pageSize,
|
||||
isLinearized,
|
||||
] = await Promise.all([
|
||||
this._fileNameLookup(),
|
||||
this.#parseFileSize(contentLength),
|
||||
this._titleLookup(),
|
||||
this.#parseDate(metadata?.get("xmp:createdate"), info.CreationDate),
|
||||
this.#parseDate(metadata?.get("xmp:modifydate"), info.ModDate),
|
||||
this.#parsePageSize(pdfPage, pagesRotation),
|
||||
this.#parseLinearization(info.IsLinearized),
|
||||
]);
|
||||
|
||||
this.#fieldData = Object.freeze({
|
||||
fileName,
|
||||
fileSize,
|
||||
title,
|
||||
author: metadata?.get("dc:creator")?.join("\n") || info.Author,
|
||||
subject: metadata?.get("dc:subject")?.join("\n") || info.Subject,
|
||||
keywords: metadata?.get("pdf:keywords") || info.Keywords,
|
||||
creationDate,
|
||||
modificationDate,
|
||||
creator: metadata?.get("xmp:creatortool") || info.Creator,
|
||||
producer: metadata?.get("pdf: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();
|
||||
}
|
||||
if (!pdfDocument) {
|
||||
return;
|
||||
}
|
||||
this.pdfDocument = pdfDocument;
|
||||
|
||||
this._dataAvailableCapability.resolve();
|
||||
}
|
||||
|
||||
#reset() {
|
||||
this.pdfDocument = null;
|
||||
|
||||
this.#fieldData = null;
|
||||
this._dataAvailableCapability = Promise.withResolvers();
|
||||
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 "-".
|
||||
*/
|
||||
#updateUI() {
|
||||
if (this.#fieldData && this.overlayManager.active !== this.dialog) {
|
||||
// Don't bother updating the dialog if it's already been closed,
|
||||
// unless it's being reset (i.e. `this.#fieldData === null`),
|
||||
// since it will be updated the next time `this.open` is called.
|
||||
return;
|
||||
}
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
this.dialog.dataset.fieldDataLastUpdated = this._fieldDataLastUpdated;
|
||||
}
|
||||
for (const id in this.fields) {
|
||||
const content = this.#fieldData?.[id];
|
||||
this.fields[id].textContent = content || content === 0 ? content : "-";
|
||||
}
|
||||
}
|
||||
|
||||
async #parseFileSize(b = 0) {
|
||||
const kb = b / 1024,
|
||||
mb = kb / 1024;
|
||||
return kb
|
||||
? this.l10n.get(
|
||||
mb >= 1
|
||||
? "pdfjs-document-properties-size-mb"
|
||||
: "pdfjs-document-properties-size-kb",
|
||||
{ mb, kb, b }
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async #parsePageSize(pdfPage, pagesRotation) {
|
||||
if (!pdfPage) {
|
||||
return undefined;
|
||||
}
|
||||
let pageSizeInches = getPageSizeInches(pdfPage);
|
||||
// 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),
|
||||
nonMetric = NON_METRIC_LOCALES.includes(this.l10n.getLanguage());
|
||||
|
||||
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 nameId =
|
||||
getPageName(sizeInches, isPortrait, US_PAGE_NAMES) ||
|
||||
getPageName(sizeMillimeters, isPortrait, METRIC_PAGE_NAMES);
|
||||
|
||||
if (
|
||||
!nameId &&
|
||||
!(
|
||||
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
|
||||
) {
|
||||
nameId = getPageName(intMillimeters, isPortrait, METRIC_PAGE_NAMES);
|
||||
if (nameId) {
|
||||
// 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([
|
||||
nonMetric ? sizeInches : sizeMillimeters,
|
||||
this.l10n.get(
|
||||
nonMetric
|
||||
? "pdfjs-document-properties-page-size-unit-inches"
|
||||
: "pdfjs-document-properties-page-size-unit-millimeters"
|
||||
),
|
||||
nameId && this.l10n.get(nameId),
|
||||
this.l10n.get(
|
||||
isPortrait
|
||||
? "pdfjs-document-properties-page-size-orientation-portrait"
|
||||
: "pdfjs-document-properties-page-size-orientation-landscape"
|
||||
),
|
||||
]);
|
||||
|
||||
return this.l10n.get(
|
||||
name
|
||||
? "pdfjs-document-properties-page-size-dimension-name-string"
|
||||
: "pdfjs-document-properties-page-size-dimension-string",
|
||||
{ width, height, unit, name, orientation }
|
||||
);
|
||||
}
|
||||
|
||||
async #parseDate(metadataDate, infoDate) {
|
||||
const dateObj =
|
||||
Date.parse(metadataDate) || PDFDateString.toDateObject(infoDate);
|
||||
return dateObj
|
||||
? this.l10n.get("pdfjs-document-properties-date-time-string", {
|
||||
dateObj: dateObj.valueOf(),
|
||||
})
|
||||
: undefined;
|
||||
}
|
||||
|
||||
#parseLinearization(isLinearized) {
|
||||
return this.l10n.get(
|
||||
isLinearized
|
||||
? "pdfjs-document-properties-linearized-yes"
|
||||
: "pdfjs-document-properties-linearized-no"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { PDFDocumentProperties };
|
||||
Reference in New Issue
Block a user