Prefer the /Metadata, when available, in the document properties dialog (bug 1966086)

This commit is contained in:
Jonas Jenwald 2025-05-15 10:26:29 +02:00
parent d4d0081ac9
commit ab89773e49
7 changed files with 148 additions and 36 deletions

View File

@ -0,0 +1,97 @@
/* Copyright 2024 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 { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
const FIELDS = [
"fileName",
"fileSize",
"title",
"author",
"subject",
"keywords",
"creationDate",
"modificationDate",
"creator",
"producer",
"version",
"pageCount",
"pageSize",
"linearized",
];
describe("PDFDocumentProperties", () => {
async function getFieldProperties(page) {
const promises = [];
for (const name of FIELDS) {
promises.push(
page.evaluate(
n => [n, document.getElementById(`${n}Field`).textContent],
name
)
);
}
return Object.fromEntries(await Promise.all(promises));
}
describe("Document with both /Info and /Metadata", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
await page.click("#documentProperties");
await page.waitForSelector("#documentPropertiesDialog", {
hidden: false,
});
await page.waitForFunction(
`document.getElementById("fileSizeField").textContent !== "-"`
);
const props = await getFieldProperties(page);
expect(props).toEqual({
fileName: "basicapi.pdf",
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
title: "Basic API Test",
author: "Brendan Dahl",
subject: "-",
keywords: "TCPDF",
creationDate: "4/10/12, 7:30:26 AM",
modificationDate: "4/10/12, 7:30:26 AM",
creator: "TCPDF",
producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
version: "1.7",
pageCount: "3",
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
linearized: "No",
});
})
);
});
});
});

View File

@ -13,7 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { closePages, loadAndWait } from "./test_utils.mjs"; import { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
function fuzzyMatch(a, b, browserName, pixelFuzz = 3) { function fuzzyMatch(a, b, browserName, pixelFuzz = 3) {
expect(a) expect(a)
@ -110,10 +110,6 @@ describe("find bar", () => {
); );
const resultElement = await page.waitForSelector("#findResultsCount"); const resultElement = await page.waitForSelector("#findResultsCount");
const resultText = await resultElement.evaluate(el => el.textContent); const resultText = await resultElement.evaluate(el => el.textContent);
/** Unicode bidi isolation characters. */
const FSI = "\u2068";
const PDI = "\u2069";
// Fluent adds these markers to the result text.
expect(resultText).toEqual(`${FSI}1${PDI} of ${FSI}1${PDI} match`); expect(resultText).toEqual(`${FSI}1${PDI} of ${FSI}1${PDI} match`);
const selectedElement = await page.waitForSelector( const selectedElement = await page.waitForSelector(
".highlight.selected" ".highlight.selected"

View File

@ -31,6 +31,7 @@ async function runTests(results) {
"autolinker_spec.mjs", "autolinker_spec.mjs",
"caret_browsing_spec.mjs", "caret_browsing_spec.mjs",
"copy_paste_spec.mjs", "copy_paste_spec.mjs",
"document_properties_spec.mjs",
"find_spec.mjs", "find_spec.mjs",
"freetext_editor_spec.mjs", "freetext_editor_spec.mjs",
"highlight_editor_spec.mjs", "highlight_editor_spec.mjs",

View File

@ -17,10 +17,12 @@ import {
awaitPromise, awaitPromise,
closePages, closePages,
copy, copy,
FSI,
getEditorSelector, getEditorSelector,
getRect, getRect,
loadAndWait, loadAndWait,
paste, paste,
PDI,
switchToEditor, switchToEditor,
waitForPointerUp, waitForPointerUp,
waitForTimeout, waitForTimeout,
@ -188,7 +190,7 @@ describe("Signature Editor", () => {
// Check the aria description. // Check the aria description.
await page.waitForSelector( await page.waitForSelector(
`${editorSelector}[aria-description="Signature editor: \u2068Hello World\u2069"]` `${editorSelector}[aria-description="Signature editor: ${FSI}Hello World${PDI}"]`
); );
// Edit the description. // Edit the description.

View File

@ -882,6 +882,10 @@ async function moveEditor(page, selector, n, pressKey) {
} }
} }
// Unicode bidi isolation characters, Fluent adds these markers to the text.
const FSI = "\u2068";
const PDI = "\u2069";
export { export {
applyFunctionToEditor, applyFunctionToEditor,
awaitPromise, awaitPromise,
@ -894,6 +898,7 @@ export {
createPromise, createPromise,
dragAndDrop, dragAndDrop,
firstPageOnTop, firstPageOnTop,
FSI,
getAnnotationSelector, getAnnotationSelector,
getAnnotationStorage, getAnnotationStorage,
getComputedStyleSelector, getComputedStyleSelector,
@ -929,6 +934,7 @@ export {
moveEditor, moveEditor,
paste, paste,
pasteFromClipboard, pasteFromClipboard,
PDI,
scrollIntoView, scrollIntoView,
selectEditor, selectEditor,
selectEditors, selectEditors,

View File

@ -598,7 +598,8 @@ const PDFViewerApplication = {
overlayManager, overlayManager,
eventBus, eventBus,
l10n, l10n,
/* fileNameLookup = */ () => this._docFilename /* fileNameLookup = */ () => this._docFilename,
/* titleLookup = */ () => this._docTitle
); );
} }
@ -1003,6 +1004,23 @@ const PDFViewerApplication = {
return this._contentDispositionFilename || getPdfFilenameFromUrl(this.url); return this._contentDispositionFilename || getPdfFilenameFromUrl(this.url);
}, },
get _docTitle() {
const { documentInfo, metadata } = this;
const title = metadata?.get("dc:title");
if (title) {
// Ghostscript can produce invalid 'dc:title' Metadata entries:
// - The title may be "Untitled" (fixes bug 1031612).
// - The title may contain incorrectly encoded characters, which thus
// looks broken, hence we ignore the Metadata entry when it contains
// characters from the Specials Unicode block (fixes bug 1605526).
if (title !== "Untitled" && !/[\uFFF0-\uFFFF]/g.test(title)) {
return title;
}
}
return documentInfo.Title;
},
/** /**
* @private * @private
*/ */
@ -1621,25 +1639,12 @@ const PDFViewerApplication = {
// Provides some basic debug information // Provides some basic debug information
console.log( console.log(
`PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` + `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
`${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` + `${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` +
`(PDF.js: ${version || "?"} [${build || "?"}])` `${(metadata?.get("xmp:creatortool") || info.Creator || "-").trim()}` +
`] (PDF.js: ${version || "?"} [${build || "?"}])`
); );
let pdfTitle = info.Title; const pdfTitle = this._docTitle;
const metadataTitle = metadata?.get("dc:title");
if (metadataTitle) {
// Ghostscript can produce invalid 'dc:title' Metadata entries:
// - The title may be "Untitled" (fixes bug 1031612).
// - The title may contain incorrectly encoded characters, which thus
// looks broken, hence we ignore the Metadata entry when it contains
// characters from the Specials Unicode block (fixes bug 1605526).
if (
metadataTitle !== "Untitled" &&
!/[\uFFF0-\uFFFF]/g.test(metadataTitle)
) {
pdfTitle = metadataTitle;
}
}
if (pdfTitle) { if (pdfTitle) {
this.setTitle( this.setTitle(
`${pdfTitle} - ${this._contentDispositionFilename || this._title}` `${pdfTitle} - ${this._contentDispositionFilename || this._title}`

View File

@ -67,13 +67,15 @@ class PDFDocumentProperties {
overlayManager, overlayManager,
eventBus, eventBus,
l10n, l10n,
fileNameLookup fileNameLookup,
titleLookup
) { ) {
this.dialog = dialog; this.dialog = dialog;
this.fields = fields; this.fields = fields;
this.overlayManager = overlayManager; this.overlayManager = overlayManager;
this.l10n = l10n; this.l10n = l10n;
this._fileNameLookup = fileNameLookup; this._fileNameLookup = fileNameLookup;
this._titleLookup = titleLookup;
this.#reset(); this.#reset();
// Bind the event listener for the Close button. // Bind the event listener for the Close button.
@ -113,7 +115,7 @@ class PDFDocumentProperties {
// Get the document properties. // Get the document properties.
const [ const [
{ info, /* metadata, contentDispositionFilename, */ contentLength }, { info, metadata, /* contentDispositionFilename, */ contentLength },
pdfPage, pdfPage,
] = await Promise.all([ ] = await Promise.all([
this.pdfDocument.getMetadata(), this.pdfDocument.getMetadata(),
@ -123,6 +125,7 @@ class PDFDocumentProperties {
const [ const [
fileName, fileName,
fileSize, fileSize,
title,
creationDate, creationDate,
modificationDate, modificationDate,
pageSize, pageSize,
@ -130,8 +133,9 @@ class PDFDocumentProperties {
] = await Promise.all([ ] = await Promise.all([
this._fileNameLookup(), this._fileNameLookup(),
this.#parseFileSize(contentLength), this.#parseFileSize(contentLength),
this.#parseDate(info.CreationDate), this._titleLookup(),
this.#parseDate(info.ModDate), this.#parseDate(metadata?.get("xmp:createdate"), info.CreationDate),
this.#parseDate(metadata?.get("xmp:modifydate"), info.ModDate),
this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation), this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation),
this.#parseLinearization(info.IsLinearized), this.#parseLinearization(info.IsLinearized),
]); ]);
@ -139,14 +143,14 @@ class PDFDocumentProperties {
this.#fieldData = Object.freeze({ this.#fieldData = Object.freeze({
fileName, fileName,
fileSize, fileSize,
title: info.Title, title,
author: info.Author, author: metadata?.get("dc:creator")?.join("\n") || info.Author,
subject: info.Subject, subject: metadata?.get("dc:subject")?.join("\n") || info.Subject,
keywords: info.Keywords, keywords: metadata?.get("pdf:keywords") || info.Keywords,
creationDate, creationDate,
modificationDate, modificationDate,
creator: info.Creator, creator: metadata?.get("xmp:creatortool") || info.Creator,
producer: info.Producer, producer: metadata?.get("pdf:producer") || info.Producer,
version: info.PDFFormatVersion, version: info.PDFFormatVersion,
pageCount: this.pdfDocument.numPages, pageCount: this.pdfDocument.numPages,
pageSize, pageSize,
@ -324,8 +328,9 @@ class PDFDocumentProperties {
); );
} }
async #parseDate(inputDate) { async #parseDate(metadataDate, infoDate) {
const dateObj = PDFDateString.toDateObject(inputDate); const dateObj =
Date.parse(metadataDate) || PDFDateString.toDateObject(infoDate);
return dateObj return dateObj
? this.l10n.get("pdfjs-document-properties-date-time-string", { ? this.l10n.get("pdfjs-document-properties-date-time-string", {
dateObj: dateObj.valueOf(), dateObj: dateObj.valueOf(),