diff --git a/test/integration/document_properties_spec.mjs b/test/integration/document_properties_spec.mjs new file mode 100644 index 000000000..3cb3295b3 --- /dev/null +++ b/test/integration/document_properties_spec.mjs @@ -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", + }); + }) + ); + }); + }); +}); diff --git a/test/integration/find_spec.mjs b/test/integration/find_spec.mjs index 257f357cd..2f3ef1e15 100644 --- a/test/integration/find_spec.mjs +++ b/test/integration/find_spec.mjs @@ -13,7 +13,7 @@ * 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) { expect(a) @@ -110,10 +110,6 @@ describe("find bar", () => { ); const resultElement = await page.waitForSelector("#findResultsCount"); 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`); const selectedElement = await page.waitForSelector( ".highlight.selected" diff --git a/test/integration/jasmine-boot.js b/test/integration/jasmine-boot.js index f4bc84a87..673dd6b07 100644 --- a/test/integration/jasmine-boot.js +++ b/test/integration/jasmine-boot.js @@ -31,6 +31,7 @@ async function runTests(results) { "autolinker_spec.mjs", "caret_browsing_spec.mjs", "copy_paste_spec.mjs", + "document_properties_spec.mjs", "find_spec.mjs", "freetext_editor_spec.mjs", "highlight_editor_spec.mjs", diff --git a/test/integration/signature_editor_spec.mjs b/test/integration/signature_editor_spec.mjs index 490068ffe..5fd90db74 100644 --- a/test/integration/signature_editor_spec.mjs +++ b/test/integration/signature_editor_spec.mjs @@ -17,10 +17,12 @@ import { awaitPromise, closePages, copy, + FSI, getEditorSelector, getRect, loadAndWait, paste, + PDI, switchToEditor, waitForPointerUp, waitForTimeout, @@ -188,7 +190,7 @@ describe("Signature Editor", () => { // Check the aria description. await page.waitForSelector( - `${editorSelector}[aria-description="Signature editor: \u2068Hello World\u2069"]` + `${editorSelector}[aria-description="Signature editor: ${FSI}Hello World${PDI}"]` ); // Edit the description. diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 90e137ada..e6752aff1 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -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 { applyFunctionToEditor, awaitPromise, @@ -894,6 +898,7 @@ export { createPromise, dragAndDrop, firstPageOnTop, + FSI, getAnnotationSelector, getAnnotationStorage, getComputedStyleSelector, @@ -929,6 +934,7 @@ export { moveEditor, paste, pasteFromClipboard, + PDI, scrollIntoView, selectEditor, selectEditors, diff --git a/web/app.js b/web/app.js index e2da40c78..0247762e3 100644 --- a/web/app.js +++ b/web/app.js @@ -598,7 +598,8 @@ const PDFViewerApplication = { overlayManager, eventBus, l10n, - /* fileNameLookup = */ () => this._docFilename + /* fileNameLookup = */ () => this._docFilename, + /* titleLookup = */ () => this._docTitle ); } @@ -1003,6 +1004,23 @@ const PDFViewerApplication = { 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 */ @@ -1621,25 +1639,12 @@ const PDFViewerApplication = { // Provides some basic debug information console.log( `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` + - `${(info.Producer || "-").trim()} / ${(info.Creator || "-").trim()}] ` + - `(PDF.js: ${version || "?"} [${build || "?"}])` + `${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` + + `${(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) { this.setTitle( `${pdfTitle} - ${this._contentDispositionFilename || this._title}` diff --git a/web/pdf_document_properties.js b/web/pdf_document_properties.js index 58cdee4da..bd3ec9cbb 100644 --- a/web/pdf_document_properties.js +++ b/web/pdf_document_properties.js @@ -67,13 +67,15 @@ class PDFDocumentProperties { overlayManager, eventBus, l10n, - fileNameLookup + 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. @@ -113,7 +115,7 @@ class PDFDocumentProperties { // Get the document properties. const [ - { info, /* metadata, contentDispositionFilename, */ contentLength }, + { info, metadata, /* contentDispositionFilename, */ contentLength }, pdfPage, ] = await Promise.all([ this.pdfDocument.getMetadata(), @@ -123,6 +125,7 @@ class PDFDocumentProperties { const [ fileName, fileSize, + title, creationDate, modificationDate, pageSize, @@ -130,8 +133,9 @@ class PDFDocumentProperties { ] = await Promise.all([ this._fileNameLookup(), this.#parseFileSize(contentLength), - this.#parseDate(info.CreationDate), - this.#parseDate(info.ModDate), + this._titleLookup(), + this.#parseDate(metadata?.get("xmp:createdate"), info.CreationDate), + this.#parseDate(metadata?.get("xmp:modifydate"), info.ModDate), this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation), this.#parseLinearization(info.IsLinearized), ]); @@ -139,14 +143,14 @@ class PDFDocumentProperties { this.#fieldData = Object.freeze({ fileName, fileSize, - title: info.Title, - author: info.Author, - subject: info.Subject, - keywords: info.Keywords, + 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: info.Creator, - producer: info.Producer, + creator: metadata?.get("xmp:creatortool") || info.Creator, + producer: metadata?.get("pdf:producer") || info.Producer, version: info.PDFFormatVersion, pageCount: this.pdfDocument.numPages, pageSize, @@ -324,8 +328,9 @@ class PDFDocumentProperties { ); } - async #parseDate(inputDate) { - const dateObj = PDFDateString.toDateObject(inputDate); + 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(),