From 62b88aa56e536234bc2fb0e5ec5c13004a7874d7 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Thu, 14 May 2026 13:46:25 +0200 Subject: [PATCH 1/6] Use Puppeteer's `ElementHandle.boundingBox()` API in the integration tests The custom solution for obtaining the bounding box of a given element that we have now was necessary during the original introduction of the integration tests because at the time the `ElementHandle.boundingBox()` API in Puppeteer didn't work correctly in Chrome. However, `getRect`, where this is used, is a hot utility function because most tests call it multiple times, either directly or indirectly via other utility functions, and it turns out that the approach we use is slower than the native `ElementHandle.boundingBox()` API. Fortunately, most likely after a combination of Chrome/Puppeteer updates and the conversion to the formalized WebDriver BiDi protocol the custom solution is no longer necessary because all tests pass without it too, so this commit converts `getRect` to use `ElementHandle.boundingBox()` instead to speed up the tests. --- test/integration/test_utils.mjs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 4146fcd7e6295..2ef42d68e222b 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -268,13 +268,8 @@ function getSelector(id) { } async function getRect(page, selector) { - // In Chrome something is wrong when serializing a `DomRect`, - // so we extract the values and return them ourselves. await page.waitForSelector(selector, { visible: true }); - return page.$eval(selector, el => { - const { x, y, width, height } = el.getBoundingClientRect(); - return { x, y, width, height }; - }); + return (await page.$(selector)).boundingBox(); } function getQuerySelector(id) { From c66f9f24971d9f6a753133006c61e5af6cfd2d38 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Thu, 14 May 2026 13:47:03 +0200 Subject: [PATCH 2/6] Reduce the protocol delay for Chrome in the integration tests Originally we introduced a small delay for Puppeteer operations in Chrome to avoid intermittent failures where protocol calls were happening too quickly in succession. However, since then a number of improvements were made, both locally and upstream, that reduce the need for this delay: - the integration tests have been hardened to remove (potential) sources of intermittent failures in many places; - the browsers and Puppeteer have been updated to improve performance and support for testing infrastructure; - the conversion to WebDriver BiDi has been completed, which replaced the Chrome-specific CDP protocol with a formalized protocol that could provide more safety guarantees. This commit therefore reduces the Chrome-specific delay from 5 to 3 milliseconds, which should nowadays be a better value to speed up the Chrome tests and bring them closer to Firefox in terms of runtime. --- test/test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.mjs b/test/test.mjs index fde169a818bed..d2adf6c3b6f50 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -992,7 +992,7 @@ async function startBrowser({ // calls can run before events triggered by the previous protocol calls had // a chance to be processed (essentially causing events to get lost). This // value gives Chrome a more similar execution speed as Firefox. - options.slowMo = 5; + options.slowMo = 3; // avoid crash options.args = ["--no-sandbox", "--disable-setuid-sandbox"]; From 600a4bb1eef52a09ec110b7918e2a0e936e66add Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 16 May 2026 16:48:45 +0200 Subject: [PATCH 3/6] Fix missing page closing for two viewer integration tests This caused the tab to remain open after the tests ran, which meant that a total of ~120 MB of memory was not being freed. --- test/integration/viewer_spec.mjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index deb059ef139f4..5dd5b2d31f9b9 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -1403,6 +1403,10 @@ describe("PDF viewer", () => { ); }); + afterEach(async () => { + await closePages(pages); + }); + it("keeps the content under the pinch centre fixed on the screen", async () => { await Promise.all( pages.map(async ([browserName, page]) => { @@ -1610,6 +1614,10 @@ describe("PDF viewer", () => { ); }); + afterEach(async () => { + await closePages(pages); + }); + it("Check that the top right corner of the annotation is centered vertically", async () => { await Promise.all( pages.map(async ([browserName, page]) => { From af65d7f930a447a585a872f5133799b04fc12907 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 17 May 2026 14:16:22 +0200 Subject: [PATCH 4/6] Enable the `must check that an existing highlight is ignored on hovering` integration test on Windows It looks like this test passes consistently again, most likely after a combination of browser/Puppeteer/configuration updates and the completed switch to the WebDriver BiDi protocol. Fixes #20136. --- test/integration/highlight_editor_spec.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 757ea2e744624..ec4e7d130185f 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -1726,10 +1726,6 @@ describe("Highlight Editor", () => { it("must check that an existing highlight is ignored on hovering", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - if (navigator.platform.includes("Win")) { - pending("Fails consistently on Windows (issue #20136)."); - } - await switchToHighlight(page); const rect = await getSpanRectFromText( From 2cef900834838f558ff0cc52e1888139d866d2b7 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 17 May 2026 16:47:03 +0200 Subject: [PATCH 5/6] Add an integration-test for documents without `contentLength` available in the `PDFDocumentProperties` dialog --- test/integration/document_properties_spec.mjs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/integration/document_properties_spec.mjs b/test/integration/document_properties_spec.mjs index 6a3dbb3e3363b..cb3d08db6cf68 100644 --- a/test/integration/document_properties_spec.mjs +++ b/test/integration/document_properties_spec.mjs @@ -14,6 +14,7 @@ */ import { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs"; +import fs from "fs/promises"; const FIELDS = [ "fileName", @@ -155,4 +156,68 @@ describe("PDFDocumentProperties", () => { ); }); }); + + describe("Document without contentLength", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("empty.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]) => { + // Open a binary PDF document, such that `contentLength` is undefined. + const base64 = await fs.readFile("./pdfs/clippath.pdf", { + encoding: "base64", + }); + + await page.evaluate(async b64 => { + await window.PDFViewerApplication.open({ + data: Uint8Array.fromBase64(b64), + }); + }, base64); + + 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: "document.pdf", + fileSize: `${FSI}0.448${PDI} KB (${FSI}459${PDI} bytes)`, + title: "-", + author: "-", + subject: "-", + keywords: "-", + creationDate: "-", + modificationDate: "-", + creator: "-", + producer: "-", + version: "1.1", + pageCount: "1", + pageSize: `${FSI}2.78${PDI} × ${FSI}1.39${PDI} ${FSI}in${PDI} (${FSI}landscape${PDI})`, + linearized: "No", + }); + + await page.click("#documentPropertiesClose"); + await page.waitForSelector("#documentPropertiesDialog", { + hidden: true, + }); + }) + ); + }); + }); }); From cd8a78c4e209d1d7d492022d29913ba3931804fc Mon Sep 17 00:00:00 2001 From: calixteman Date: Sun, 17 May 2026 15:53:45 +0200 Subject: [PATCH 6/6] Recover CFF private dict defaults zeroed by Ghostscript It fixes the issue #20633. --- src/core/cff_parser.js | 47 +++++++++++++++++++---- test/unit/cff_parser_spec.js | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/core/cff_parser.js b/src/core/cff_parser.js index 50b42ea2d5ea2..c6655de89ac59 100644 --- a/src/core/cff_parser.js +++ b/src/core/cff_parser.js @@ -108,6 +108,11 @@ const CFFStandardStrings = [ const NUM_STANDARD_CFF_STRINGS = 391; +const DEFAULT_BLUE_SCALE = 0.039625; +const DEFAULT_BLUE_SHIFT = 7; +const DEFAULT_BLUE_FUZZ = 1; +const DEFAULT_EXPANSION_FACTOR = 0.06; + const CharstringValidationData = [ /* 0 */ null, /* 1 */ { id: "hstem", min: 2, stackClearing: true, stem: true }, @@ -262,8 +267,16 @@ class CFFParser { properties.fontMatrix = fontMatrix; } - const fontBBox = topDict.getByName("FontBBox"); - if (fontBBox) { + let fontBBox = topDict.getByName("FontBBox"); + if (fontBBox?.every(coord => coord === 0) && properties.bbox) { + fontBBox = Util.normalizeRect( + properties.bbox.map(coord => + coord > 0x7fff && coord <= 0xffff ? coord - 0x10000 : coord + ) + ); + topDict.setByName("FontBBox", fontBBox); + } + if (fontBBox?.some(coord => coord !== 0)) { // adjusting ascent/descent properties.ascent = Math.max(fontBBox[3], fontBBox[1]); properties.descent = Math.min(fontBBox[1], fontBBox[3]); @@ -785,10 +798,28 @@ class CFFParser { ); parentDict.privateDict = privateDict; - if (privateDict.getByName("ExpansionFactor") === 0) { + const blueScale = privateDict.getByName("BlueScale"); + const blueShift = privateDict.getByName("BlueShift"); + const blueFuzz = privateDict.getByName("BlueFuzz"); + const expansionFactor = privateDict.getByName("ExpansionFactor"); + if ( + blueScale === 0 && + blueShift === 0 && + blueFuzz === 0 && + expansionFactor === 0 + ) { + // Ghostscript can fail to initialize Private DICT defaults before + // writing them, which leaves omitted blue zone values as explicit + // zeroes. This has been seen in FDArray entries. + privateDict.setByName("BlueScale", DEFAULT_BLUE_SCALE); + privateDict.setByName("BlueShift", DEFAULT_BLUE_SHIFT); + privateDict.setByName("BlueFuzz", DEFAULT_BLUE_FUZZ); + } + + if (expansionFactor === 0) { // Firefox doesn't render correctly such a font on Windows (see issue // 15289), hence we just reset it to its default value. - privateDict.setByName("ExpansionFactor", 0.06); + privateDict.setByName("ExpansionFactor", DEFAULT_EXPANSION_FACTOR); } // Parse the Subrs index also since it's relative to the private dict. @@ -1247,16 +1278,16 @@ const CFFPrivateDictLayout = [ [7, "OtherBlues", "delta", null], [8, "FamilyBlues", "delta", null], [9, "FamilyOtherBlues", "delta", null], - [[12, 9], "BlueScale", "num", 0.039625], - [[12, 10], "BlueShift", "num", 7], - [[12, 11], "BlueFuzz", "num", 1], + [[12, 9], "BlueScale", "num", DEFAULT_BLUE_SCALE], + [[12, 10], "BlueShift", "num", DEFAULT_BLUE_SHIFT], + [[12, 11], "BlueFuzz", "num", DEFAULT_BLUE_FUZZ], [10, "StdHW", "num", null], [11, "StdVW", "num", null], [[12, 12], "StemSnapH", "delta", null], [[12, 13], "StemSnapV", "delta", null], [[12, 14], "ForceBold", "num", 0], [[12, 17], "LanguageGroup", "num", 0], - [[12, 18], "ExpansionFactor", "num", 0.06], + [[12, 18], "ExpansionFactor", "num", DEFAULT_EXPANSION_FACTOR], [[12, 19], "initialRandomSeed", "num", 0], [20, "defaultWidthX", "num", 0], [21, "nominalWidthX", "num", 0], diff --git a/test/unit/cff_parser_spec.js b/test/unit/cff_parser_spec.js index c58fd65519c60..e5ddd905b53e1 100644 --- a/test/unit/cff_parser_spec.js +++ b/test/unit/cff_parser_spec.js @@ -18,7 +18,9 @@ import { CFFCompiler, CFFFDSelect, CFFParser, + CFFPrivateDict, CFFStrings, + CFFTopDict, } from "../../src/core/cff_parser.js"; import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js"; import { Stream } from "../../src/core/stream.js"; @@ -112,6 +114,77 @@ describe("CFFParser", function () { expect(topDict.getByName("Private")).toEqual([45, 102]); }); + it("ignores an empty FontBBox when adjusting ascent/descent", function () { + cff.topDict.setByName("FontBBox", [0, 0, 0, 0]); + const fontDataWithEmptyBBox = new CFFCompiler(cff).compile(); + + const properties = { + ascent: 800, + descent: -200, + }; + new CFFParser( + new Stream(fontDataWithEmptyBBox), + properties, + SEAC_ANALYSIS_ENABLED + ).parse(); + + expect(properties.ascent).toEqual(800); + expect(properties.descent).toEqual(-200); + expect(properties.ascentScaled).toBeUndefined(); + }); + + it("repairs an empty FontBBox from font descriptor data", function () { + cff.topDict.setByName("FontBBox", [0, 0, 0, 0]); + const fontDataWithEmptyBBox = new CFFCompiler(cff).compile(); + + const properties = { + bbox: [2974, -300, 64236, 900], + }; + const reparsedCff = new CFFParser( + new Stream(fontDataWithEmptyBBox), + properties, + SEAC_ANALYSIS_ENABLED + ).parse(); + + expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([ + -1300, -300, 2974, 900, + ]); + expect(properties.ascent).toEqual(900); + expect(properties.descent).toEqual(-300); + expect(properties.ascentScaled).toEqual(true); + }); + + it("repairs likely Ghostscript-zeroed FDArray private defaults", function () { + cff.isCIDFont = true; + cff.topDict.setByName("ROS", [0, 0, 0]); + cff.topDict.setByName("FDSelect", 0); + cff.topDict.setByName("FDArray", 0); + + const fdDict = new CFFTopDict(cff.strings); + fdDict.setByName("Private", [0, 0]); + fdDict.privateDict = new CFFPrivateDict(cff.strings); + fdDict.privateDict.setByName("BlueScale", 0); + fdDict.privateDict.setByName("BlueShift", 0); + fdDict.privateDict.setByName("BlueFuzz", 0); + fdDict.privateDict.setByName("ExpansionFactor", 0); + + cff.fdArray = [fdDict]; + cff.fdSelect = new CFFFDSelect(0, Array(cff.charStrings.count).fill(0)); + const fontDataWithBrokenFDPrivate = new CFFCompiler(cff).compile(); + + const reparsedCff = new CFFParser( + new Stream(fontDataWithBrokenFDPrivate), + {}, + SEAC_ANALYSIS_ENABLED + ).parse(); + const privateDict = reparsedCff.fdArray[0].privateDict; + + expect(privateDict.getByName("BlueScale")).toEqual(0.039625); + expect(privateDict.getByName("BlueShift")).toEqual(7); + expect(privateDict.getByName("BlueFuzz")).toEqual(1); + expect(privateDict.getByName("ExpansionFactor")).toEqual(0.06); + }); + it("refuses to add topDict key with invalid value (bug 1068432)", function () { const topDict = cff.topDict; const defaultValue = topDict.getByName("UnderlinePosition");