diff --git a/gulpfile.mjs b/gulpfile.mjs index 5bd6d4b740456..d06573055068b 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -1214,6 +1214,10 @@ function discardCommentsCSS() { } function preprocessHTML(source, defines) { + defines = { + ...defines, + TESTING: defines.TESTING ?? process.env.TESTING === "true", + }; const outName = getTempFile("~preprocess", ".html"); preprocess(source, outName, defines); const out = fs.readFileSync(outName).toString(); diff --git a/src/display/canvas_dependency_tracker.js b/src/display/canvas_dependency_tracker.js index 11b6a564f6505..d448fa8c80f93 100644 --- a/src/display/canvas_dependency_tracker.js +++ b/src/display/canvas_dependency_tracker.js @@ -28,6 +28,56 @@ function expandBBox(array, index, minX, minY, maxX, maxY) { array[index * 4 + 3] = Math.max(array[index * 4 + 3], maxY); } +// Apply a scaling matrix to some min/max values. +// If a scaling factor is negative then min and max must be swapped. +function scaleMinMax(transform, minMax) { + let temp; + if (transform[0]) { + if (transform[0] < 0) { + temp = minMax[0]; + minMax[0] = minMax[2]; + minMax[2] = temp; + } + minMax[0] *= transform[0]; + minMax[2] *= transform[0]; + + if (transform[3] < 0) { + temp = minMax[1]; + minMax[1] = minMax[3]; + minMax[3] = temp; + } + minMax[1] *= transform[3]; + minMax[3] *= transform[3]; + } else { + temp = minMax[0]; + minMax[0] = minMax[1]; + minMax[1] = temp; + temp = minMax[2]; + minMax[2] = minMax[3]; + minMax[3] = temp; + + if (transform[1] < 0) { + temp = minMax[1]; + minMax[1] = minMax[3]; + minMax[3] = temp; + } + minMax[1] *= transform[1]; + minMax[3] *= transform[1]; + + if (transform[2] < 0) { + temp = minMax[0]; + minMax[0] = minMax[2]; + minMax[2] = temp; + } + minMax[0] *= transform[2]; + minMax[2] *= transform[2]; + } + minMax[0] += transform[4]; + minMax[1] += transform[5]; + minMax[2] += transform[4]; + minMax[3] += transform[5]; +} + // This is computed rathter than hard-coded to keep into // account the platform's endianess. const EMPTY_BBOX = new Uint32Array(new Uint8Array([255, 255, 0, 0]).buffer)[0]; @@ -613,7 +663,7 @@ class CanvasDependencyTracker { computedBBox = [0, 0, 0, 0]; Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox); if (scale !== 1 || x !== 0 || y !== 0) { - Util.scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox); + scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox); } if (isBBoxTrustworthy) { diff --git a/src/shared/util.js b/src/shared/util.js index 88fda0439ffa8..6c36f758a4665 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -711,57 +711,6 @@ class Util { return `#${this.hexNums[r]}${this.hexNums[g]}${this.hexNums[b]}`; } - // Apply a scaling matrix to some min/max values. - // If a scaling factor is negative then min and max must be - // swapped. - static scaleMinMax(transform, minMax) { - let temp; - if (transform[0]) { - if (transform[0] < 0) { - temp = minMax[0]; - minMax[0] = minMax[2]; - minMax[2] = temp; - } - minMax[0] *= transform[0]; - minMax[2] *= transform[0]; - - if (transform[3] < 0) { - temp = minMax[1]; - minMax[1] = minMax[3]; - minMax[3] = temp; - } - minMax[1] *= transform[3]; - minMax[3] *= transform[3]; - } else { - temp = minMax[0]; - minMax[0] = minMax[1]; - minMax[1] = temp; - temp = minMax[2]; - minMax[2] = minMax[3]; - minMax[3] = temp; - - if (transform[1] < 0) { - temp = minMax[1]; - minMax[1] = minMax[3]; - minMax[3] = temp; - } - minMax[1] *= transform[1]; - minMax[3] *= transform[1]; - - if (transform[2] < 0) { - temp = minMax[0]; - minMax[0] = minMax[2]; - minMax[2] = temp; - } - minMax[0] *= transform[2]; - minMax[2] *= transform[2]; - } - minMax[0] += transform[4]; - minMax[1] += transform[5]; - minMax[2] += transform[4]; - minMax[3] += transform[5]; - } - // Concatenates two transformation matrices together and returns the result. static transform(m1, m2) { return [ diff --git a/test/integration/document_properties_spec.mjs b/test/integration/document_properties_spec.mjs index cb3d08db6cf68..e4192dd640c9b 100644 --- a/test/integration/document_properties_spec.mjs +++ b/test/integration/document_properties_spec.mjs @@ -33,21 +33,50 @@ const FIELDS = [ "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)); +async function openDocumentProperties(page) { + await page.click("#secondaryToolbarToggleButton"); + await page.waitForSelector("#secondaryToolbar", { hidden: false }); + + await page.click("#documentProperties"); + await page.waitForSelector("#documentPropertiesDialog", { + hidden: false, + }); +} + +async function closeDocumentProperties(page) { + await page.click("#documentPropertiesClose"); + await page.waitForSelector("#documentPropertiesDialog", { + hidden: true, + }); +} + +async function checkFieldProperties(page, expectedProps) { + await page.waitForFunction( + `document.getElementById("fileSizeField").textContent !== "-"` + ); + const promises = []; + + for (const name of FIELDS) { + promises.push( + page.evaluate( + n => [n, document.getElementById(`${n}Field`).textContent], + name + ) + ); } + const props = Object.fromEntries(await Promise.all(promises)); + expect(props).toEqual(expectedProps); +} + +function getFieldDataLastUpdated(page) { + return page.evaluate( + () => + document.getElementById("documentPropertiesDialog").dataset + .fieldDataLastUpdated + ); +} +describe("PDFDocumentProperties", () => { describe("Document with both /Info and /Metadata", () => { let pages; @@ -59,23 +88,12 @@ describe("PDFDocumentProperties", () => { await closePages(pages); }); - it("must check that the document properties dialog has the correct information", async () => { + it("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 openDocumentProperties(page); - await page.waitForFunction( - `document.getElementById("fileSizeField").textContent !== "-"` - ); - const props = await getFieldProperties(page); - - expect(props).toEqual({ + await checkFieldProperties(page, { fileName: "basicapi.pdf", fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`, title: "Basic API Test", @@ -92,10 +110,7 @@ describe("PDFDocumentProperties", () => { linearized: "No", }); - await page.click("#documentPropertiesClose"); - await page.waitForSelector("#documentPropertiesDialog", { - hidden: true, - }); + await closeDocumentProperties(page); }) ); }); @@ -115,23 +130,12 @@ describe("PDFDocumentProperties", () => { await closePages(pages); }); - it("must check that the document properties dialog has the correct information", async () => { + it("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 openDocumentProperties(page); - await page.waitForFunction( - `document.getElementById("fileSizeField").textContent !== "-"` - ); - const props = await getFieldProperties(page); - - expect(props).toEqual({ + await checkFieldProperties(page, { fileName: "arial_unicode_en_cidfont.pdf", fileSize: `${FSI}15.4${PDI} KB (${FSI}15,779${PDI} bytes)`, title: "-", @@ -148,10 +152,7 @@ describe("PDFDocumentProperties", () => { linearized: "No", }); - await page.click("#documentPropertiesClose"); - await page.waitForSelector("#documentPropertiesDialog", { - hidden: true, - }); + await closeDocumentProperties(page); }) ); }); @@ -168,7 +169,7 @@ describe("PDFDocumentProperties", () => { await closePages(pages); }); - it("must check that the document properties dialog has the correct information", async () => { + it("check that the document properties dialog has the correct information", async () => { await Promise.all( pages.map(async ([browserName, page]) => { // Open a binary PDF document, such that `contentLength` is undefined. @@ -182,20 +183,9 @@ describe("PDFDocumentProperties", () => { }); }, base64); - await page.click("#secondaryToolbarToggleButton"); - await page.waitForSelector("#secondaryToolbar", { hidden: false }); + await openDocumentProperties(page); - 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({ + await checkFieldProperties(page, { fileName: "document.pdf", fileSize: `${FSI}0.448${PDI} KB (${FSI}459${PDI} bytes)`, title: "-", @@ -212,10 +202,226 @@ describe("PDFDocumentProperties", () => { linearized: "No", }); - await page.click("#documentPropertiesClose"); - await page.waitForSelector("#documentPropertiesDialog", { - hidden: true, + await closeDocumentProperties(page); + }) + ); + }); + }); + + describe("Document with multiple pages, and changed viewer page/rotation", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("check that the document properties dialog has the correct information", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await openDocumentProperties(page); + + await checkFieldProperties(page, { + 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", + }); + const fieldDataLastUpdated = await getFieldDataLastUpdated(page); + + await closeDocumentProperties(page); + + // Ensure that immediately re-opening the dialog doesn't cause + // the field-data to be fetched and parsed again. + await openDocumentProperties(page); + + expect(await getFieldDataLastUpdated(page)).toEqual( + fieldDataLastUpdated + ); + + await closeDocumentProperties(page); + + // Goto the second page, and rotate the document. + await page.click("#next"); + await page.waitForFunction( + () => window.PDFViewerApplication.page === 2 + ); + await page.keyboard.press("r"); + await page.waitForFunction( + () => window.PDFViewerApplication.pdfViewer.pagesRotation === 90 + ); + + await openDocumentProperties(page); + + await checkFieldProperties(page, { + 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}11.69${PDI} × ${FSI}8.27${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}landscape${PDI})`, + linearized: "No", }); + + await closeDocumentProperties(page); + }) + ); + }); + }); + + describe("Document with different page sizes", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("sizes.pdf", ".textLayer .endOfContent"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("check that the document properties dialog has the correct information", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await openDocumentProperties(page); + + await checkFieldProperties(page, { + fileName: "sizes.pdf", + fileSize: `${FSI}13.4${PDI} KB (${FSI}13,739${PDI} bytes)`, + title: "-", + author: "Yury ", + subject: "-", + keywords: "-", + creationDate: "6/26/11, 1:26:03 PM", + modificationDate: "-", + creator: "Writer", + producer: "OpenOffice.org 3.3", + version: "1.4", + pageCount: "3", + pageSize: `${FSI}8.5${PDI} × ${FSI}11${PDI} ${FSI}in${PDI} (${FSI}Letter${PDI}, ${FSI}portrait${PDI})`, + linearized: "No", + }); + + await closeDocumentProperties(page); + + // Goto the second page. + await page.click("#next"); + await page.waitForFunction( + () => window.PDFViewerApplication.page === 2 + ); + + await openDocumentProperties(page); + + await checkFieldProperties(page, { + fileName: "sizes.pdf", + fileSize: `${FSI}13.4${PDI} KB (${FSI}13,739${PDI} bytes)`, + title: "-", + author: "Yury ", + subject: "-", + keywords: "-", + creationDate: "6/26/11, 1:26:03 PM", + modificationDate: "-", + creator: "Writer", + producer: "OpenOffice.org 3.3", + version: "1.4", + pageCount: "3", + pageSize: `${FSI}9.01${PDI} × ${FSI}4.49${PDI} ${FSI}in${PDI} (${FSI}landscape${PDI})`, + linearized: "No", + }); + + await closeDocumentProperties(page); + }) + ); + }); + }); + + describe("Document with corrupt page", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "Pages-tree-refs.pdf", + ".textLayer .endOfContent", + null, + null, + { page: 2 } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("check that the document properties dialog has the correct information", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await openDocumentProperties(page); + + await checkFieldProperties(page, { + fileName: "Pages-tree-refs.pdf", + fileSize: `${FSI}1.07${PDI} KB (${FSI}1,098${PDI} bytes)`, + title: "-", + author: "-", + subject: "-", + keywords: "-", + creationDate: "-", + modificationDate: "-", + creator: "-", + producer: "-", + version: "1.7", + pageCount: "2", + pageSize: "-", + linearized: "No", + }); + + await closeDocumentProperties(page); + + // Goto the first page (which is *not* corrupt). + await page.click("#previous"); + await page.waitForFunction( + () => window.PDFViewerApplication.page === 1 + ); + + await openDocumentProperties(page); + + await checkFieldProperties(page, { + fileName: "Pages-tree-refs.pdf", + fileSize: `${FSI}1.07${PDI} KB (${FSI}1,098${PDI} bytes)`, + title: "-", + author: "-", + subject: "-", + keywords: "-", + creationDate: "-", + modificationDate: "-", + creator: "-", + producer: "-", + version: "1.7", + pageCount: "2", + pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`, + linearized: "No", + }); + + await closeDocumentProperties(page); }) ); }); diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 5dd5b2d31f9b9..a937e3cf8e442 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -26,8 +26,11 @@ import { waitForPageChanging, waitForPageRendered, } from "./test_utils.mjs"; +import path from "path"; import { PNG } from "pngjs"; +const __dirname = import.meta.dirname; + describe("PDF viewer", () => { describe("Zoom origin", () => { let pages; @@ -1900,5 +1903,108 @@ describe("PDF viewer", () => { ); }); }); + + describe("@page size stylesheet under CSP", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "basicapi.pdf", + ".textLayer .endOfContent", + null, + { + earlySetup: () => { + // Capture state while window.print() runs — the print service's + // destroy() removes the @page stylesheet right after, on the + // afterprint event. + window._pageRuleApplied = null; + window.print = () => { + window._pageRuleApplied = [ + ...document.querySelectorAll("style"), + ].some( + s => + s.sheet?.cssRules.length > 0 && + [...s.sheet.cssRules].some(r => r.cssText.includes("@page")) + ); + }; + }, + appSetup: app => { + app._testPrintResolver = Promise.withResolvers(); + }, + eventBusSetup: eventBus => { + eventBus.on( + "afterprint", + () => { + window.PDFViewerApplication._testPrintResolver.resolve(); + }, + { once: true } + ); + }, + } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + // The print service injects an inline + // to match the PDF's page + // dimensions. If the CSP `style-src-elem` directive blocks inline + // + at print time (web/pdf_print_service.js, web/firefox_print_service.js) + to match the PDF's page dimensions. Since the size varies per PDF the + content can't be pre-hashed, so style-src-elem allows 'unsafe-inline'. + Inline style="…" attributes stay blocked via style-src (no fallback). + --> + + + + diff --git a/web/viewer-snippet-chrome-overlays.html b/web/viewer-snippet-chrome-overlays.html index fff0eb2f32786..357b8510786b0 100644 --- a/web/viewer-snippet-chrome-overlays.html +++ b/web/viewer-snippet-chrome-overlays.html @@ -4,19 +4,7 @@ users with recognizing which checkbox they have to click when they visit chrome://extensions. --> -

+

Click on "Allow access to file URLs" at chrome://extensions
diff --git a/web/viewer.css b/web/viewer.css index 832439af42344..4defccd10f6d5 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -713,6 +713,25 @@ dialog :link { margin-top: 10px; } +/*#if !MOZCENTRAL*/ +#printServiceDialog { + min-width: 200px; +} +/*#endif*/ + +/*#if CHROME*/ +#chrome-pdfjs-logo-bg { + display: block; + padding-left: 60px; + min-height: 48px; + background-size: 48px; + background-repeat: no-repeat; + font-size: 14px; + line-height: 1.8em; + word-break: break-all; +} +/*#endif*/ + .grab-to-pan-grab { cursor: grab !important; } diff --git a/web/viewer.html b/web/viewer.html index 0d5d90c7d61ca..e4d24d7e4e45f 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -29,6 +29,35 @@ PDF.js viewer + + + + + + + + + + + @@ -1237,7 +1266,7 @@ -

+