mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-03-11 07:02:33 +01:00
342 lines
11 KiB
JavaScript
342 lines
11 KiB
JavaScript
/* 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("./interfaces.js").IL10n} IL10n */
|
||
/** @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 { PDFDateString } from "./pdfjs";
|
||
|
||
// 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 {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;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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),
|
||
// eslint-disable-next-line arrow-body-style
|
||
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();
|
||
}
|
||
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;
|
||
}
|
||
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
|
||
// NOTE
|
||
? mb >= 1 ? `${mb.toPrecision(3)} MB ${b} bytes` : `${kb.toPrecision(3)} KB ${b} bytes`
|
||
: undefined;
|
||
}
|
||
|
||
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),
|
||
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,
|
||
// NOTE
|
||
this.l10n.get(
|
||
nonMetric
|
||
? "unitInches"
|
||
: "unitMillimeters"
|
||
),
|
||
nameId && this.l10n.get(nameId),
|
||
this.l10n.get(
|
||
isPortrait
|
||
? "document_properties_page_size_orientation_portrait"
|
||
: "document_properties_page_size_orientation_landscape"
|
||
),
|
||
]);
|
||
|
||
// NOTE
|
||
return name ?`${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${name}, ${orientation})`:
|
||
`${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${orientation})`;
|
||
}
|
||
|
||
async #parseDate(inputDate) {
|
||
const dateObj = PDFDateString.toDateObject(inputDate);
|
||
return dateObj
|
||
// NOTE
|
||
? `${dateObj.toLocaleDateString()}, ${dateObj.toLocaleTimeString()}`
|
||
: undefined;
|
||
}
|
||
|
||
#parseLinearization(isLinearized) {
|
||
return this.l10n.get(
|
||
isLinearized
|
||
// NOTE
|
||
? "Yes"
|
||
: "No"
|
||
);
|
||
}
|
||
}
|
||
|
||
export { PDFDocumentProperties };
|