From fb9758303bf69c91ce5e0f60f408de2f9c9103de Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 21 May 2026 18:27:43 +0200 Subject: [PATCH 1/6] Add support for Optional Content in the AnnotationLayer (issue 20433) --- src/core/annotation.js | 41 +++++------ src/core/evaluator.js | 100 +------------------------- src/core/evaluator_utils.js | 123 ++++++++++++++++++++++++++++++++ src/display/annotation_layer.js | 24 ++++++- test/driver.js | 7 +- test/pdfs/issue20433.pdf.link | 1 + test/test_manifest.json | 36 ++++++++++ web/annotation_layer_builder.js | 24 +++++-- web/pdf_page_view.js | 1 + 9 files changed, 225 insertions(+), 132 deletions(-) create mode 100644 src/core/evaluator_utils.js create mode 100644 test/pdfs/issue20433.pdf.link diff --git a/src/core/annotation.js b/src/core/annotation.js index 2befa96e9940f..292fb10c5ec85 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -76,6 +76,7 @@ import { FileSpec } from "./file_spec.js"; import { JpegStream } from "./jpeg_stream.js"; import { ObjectLoader } from "./object_loader.js"; import { OperatorList } from "./operator_list.js"; +import { parseMarkedContentProps } from "./evaluator_utils.js"; import { XFAFactory } from "./xfa/factory.js"; class AnnotationFactory { @@ -663,6 +664,8 @@ function getTransformMatrix(rect, bbox, matrix) { } class Annotation { + _oc = undefined; + constructor(params) { const { annotationGlobals, dict, orphanFields, ref, subtype, xref } = params; @@ -679,7 +682,7 @@ class Annotation { this.setColor(dict.getArray("C")); this.setBorderStyle(dict); this.setAppearance(dict); - this.setOptionalContent(dict); + this.#setOptionalContent(xref, dict); const MK = dict.get("MK"); this.setBorderAndBackgroundColors(MK); @@ -710,6 +713,7 @@ class Annotation { hasAppearance: !!this.appearance, id: params.id, modificationDate: this.modificationDate, + oc: this._oc, rect: this.rectangle, subtype, hasOwnCanvas: false, @@ -1169,14 +1173,17 @@ class Annotation { } } - setOptionalContent(dict) { - this.oc = null; - - const oc = dict.get("OC"); - if (oc instanceof Name) { - warn("setOptionalContent: Support for /Name-entry is not implemented."); - } else if (oc instanceof Dict) { - this.oc = oc; + #setOptionalContent(xref, dict) { + if (dict.has("OC")) { + try { + this._oc = parseMarkedContentProps( + xref, + dict.get("OC"), + /* resources = */ null + ); + } catch (ex) { + warn(`#setOptionalContent: ${ex}`); + } } } @@ -1229,13 +1236,7 @@ class Annotation { const opList = new OperatorList(); - let optionalContent; - if (this.oc) { - optionalContent = await evaluator.parseMarkedContentProps( - this.oc, - /* resources = */ null - ); - } + const optionalContent = this._oc; if (optionalContent !== undefined) { opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]); } @@ -2110,13 +2111,7 @@ class WidgetAnnotation extends Annotation { const bbox = [0, 0, this.width, this.height]; const transform = getTransformMatrix(this.data.rect, bbox, matrix); - let optionalContent; - if (this.oc) { - optionalContent = await evaluator.parseMarkedContentProps( - this.oc, - /* resources = */ null - ); - } + const optionalContent = this._oc; if (optionalContent !== undefined) { opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]); } diff --git a/src/core/evaluator.js b/src/core/evaluator.js index e1badcec3670b..1c9c229bc89f0 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -87,6 +87,7 @@ import { getGlyphsUnicode } from "./glyphlist.js"; import { getMetrics } from "./metrics.js"; import { getUnicodeForGlyph } from "./unicode.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js"; +import { parseMarkedContentProps } from "./evaluator_utils.js"; import { PDFImage } from "./image.js"; import { Stream } from "./stream.js"; import { stringToPDFString } from "./string_utils.js"; @@ -1651,105 +1652,8 @@ class PartialEvaluator { throw new FormatError(`Unknown PatternName: ${patternName}`); } - _parseVisibilityExpression(array, nestingCounter, currentResult) { - const MAX_NESTING = 10; - if (++nestingCounter > MAX_NESTING) { - warn("Visibility expression is too deeply nested"); - return; - } - const length = array.length; - const operator = this.xref.fetchIfRef(array[0]); - if (length < 2 || !(operator instanceof Name)) { - warn("Invalid visibility expression"); - return; - } - switch (operator.name) { - case "And": - case "Or": - case "Not": - currentResult.push(operator.name); - break; - default: - warn(`Invalid operator ${operator.name} in visibility expression`); - return; - } - for (let i = 1; i < length; i++) { - const raw = array[i]; - const object = this.xref.fetchIfRef(raw); - if (Array.isArray(object)) { - const nestedResult = []; - currentResult.push(nestedResult); - // Recursively parse a subarray. - this._parseVisibilityExpression(object, nestingCounter, nestedResult); - } else if (raw instanceof Ref) { - // Reference to an OCG dictionary. - currentResult.push(raw.toString()); - } - } - } - async parseMarkedContentProps(contentProperties, resources) { - let optionalContent; - if (contentProperties instanceof Name) { - const properties = resources.get("Properties"); - optionalContent = properties.get(contentProperties.name); - } else if (contentProperties instanceof Dict) { - optionalContent = contentProperties; - } else { - throw new FormatError("Optional content properties malformed."); - } - - const optionalContentType = optionalContent.get("Type")?.name; - if (optionalContentType === "OCG") { - return { - type: optionalContentType, - id: optionalContent.objId, - }; - } else if (optionalContentType === "OCMD") { - const expression = optionalContent.get("VE"); - if (Array.isArray(expression)) { - const result = []; - this._parseVisibilityExpression(expression, 0, result); - if (result.length > 0) { - return { - type: "OCMD", - expression: result, - }; - } - } - - const optionalContentGroups = optionalContent.get("OCGs"); - if ( - Array.isArray(optionalContentGroups) || - optionalContentGroups instanceof Dict - ) { - const groupIds = []; - if (Array.isArray(optionalContentGroups)) { - for (const ocg of optionalContentGroups) { - groupIds.push(ocg.toString()); - } - } else { - // Dictionary, just use the obj id. - groupIds.push(optionalContentGroups.objId); - } - - return { - type: optionalContentType, - ids: groupIds, - policy: - optionalContent.get("P") instanceof Name - ? optionalContent.get("P").name - : null, - expression: null, - }; - } else if (optionalContentGroups instanceof Ref) { - return { - type: optionalContentType, - id: optionalContentGroups.toString(), - }; - } - } - return null; + return parseMarkedContentProps(this.xref, contentProperties, resources); } async getOperatorList({ diff --git a/src/core/evaluator_utils.js b/src/core/evaluator_utils.js new file mode 100644 index 0000000000000..b871bfa93fa9b --- /dev/null +++ b/src/core/evaluator_utils.js @@ -0,0 +1,123 @@ +/* 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. + */ + +import { Dict, Name, Ref } from "./primitives.js"; +import { FormatError, warn } from "../shared/util.js"; + +function _parseVisibilityExpression( + xref, + array, + nestingCounter, + currentResult +) { + const MAX_NESTING = 10; + if (++nestingCounter > MAX_NESTING) { + warn("Visibility expression is too deeply nested"); + return; + } + const length = array.length; + const operator = xref.fetchIfRef(array[0]); + if (length < 2 || !(operator instanceof Name)) { + warn("Invalid visibility expression"); + return; + } + switch (operator.name) { + case "And": + case "Or": + case "Not": + currentResult.push(operator.name); + break; + default: + warn(`Invalid operator ${operator.name} in visibility expression`); + return; + } + for (let i = 1; i < length; i++) { + const raw = array[i]; + const object = xref.fetchIfRef(raw); + if (Array.isArray(object)) { + const nestedResult = []; + currentResult.push(nestedResult); + // Recursively parse a subarray. + _parseVisibilityExpression(xref, object, nestingCounter, nestedResult); + } else if (raw instanceof Ref) { + // Reference to an OCG dictionary. + currentResult.push(raw.toString()); + } + } +} + +function parseMarkedContentProps(xref, contentProperties, resources) { + let optionalContent; + if (contentProperties instanceof Name) { + const properties = resources.get("Properties"); + optionalContent = properties.get(contentProperties.name); + } else if (contentProperties instanceof Dict) { + optionalContent = contentProperties; + } else { + throw new FormatError("Optional content properties malformed."); + } + + const optionalContentType = optionalContent.get("Type")?.name; + if (optionalContentType === "OCG") { + return { + type: optionalContentType, + id: optionalContent.objId, + }; + } else if (optionalContentType === "OCMD") { + const expression = optionalContent.get("VE"); + if (Array.isArray(expression)) { + const result = []; + _parseVisibilityExpression(xref, expression, 0, result); + if (result.length > 0) { + return { + type: "OCMD", + expression: result, + }; + } + } + + const optionalContentGroups = optionalContent.get("OCGs"); + if ( + Array.isArray(optionalContentGroups) || + optionalContentGroups instanceof Dict + ) { + const groupIds = []; + if (Array.isArray(optionalContentGroups)) { + for (const ocg of optionalContentGroups) { + groupIds.push(ocg.toString()); + } + } else { + // Dictionary, just use the obj id. + groupIds.push(optionalContentGroups.objId); + } + const p = optionalContent.get("P"); + + return { + type: optionalContentType, + ids: groupIds, + policy: p instanceof Name ? p.name : null, + expression: null, + }; + } else if (optionalContentGroups instanceof Ref) { + return { + type: optionalContentType, + id: optionalContentGroups.toString(), + }; + } + } + return null; +} + +export { parseMarkedContentProps }; diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 4e0c840aafac5..ec2999b3b1741 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -16,6 +16,8 @@ /** @typedef {import("./api").PDFPageProxy} PDFPageProxy */ /** @typedef {import("./page_viewport").PageViewport} PageViewport */ // eslint-disable-next-line max-len +/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */ +// eslint-disable-next-line max-len /** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ // eslint-disable-next-line max-len /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */ @@ -886,6 +888,18 @@ class AnnotationElement { }); } + updateOC(optionalContentConfig) { + if (!this.data.oc || !optionalContentConfig) { + return; + } + const isVisible = optionalContentConfig.isVisible(this.data.oc); + if (isVisible) { + this.show(); + } else { + this.hide(); + } + } + get width() { return this.data.rect[2] - this.data.rect[0]; } @@ -3755,7 +3769,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement { * @property {TextAccessibilityManager} [accessibilityManager] * @property {AnnotationEditorUIManager} [annotationEditorUIManager] * @property {StructTreeLayerBuilder} [structTreeLayer] - * @property {CommentManager} [commentManager] - The comment manager instance. + * @property {OptionalContentConfig} [optionalContentConfig] */ /** @@ -3827,7 +3841,7 @@ class AnnotationLayer { * @memberof AnnotationLayer */ async render(params) { - const { annotations } = params; + const { annotations, optionalContentConfig } = params; const layer = this.div; setLayerDimensions(layer, this.viewport); @@ -3892,6 +3906,7 @@ class AnnotationLayer { if (data.hidden) { rendered.style.visibility = "hidden"; } + element.updateOC(optionalContentConfig); if (element._isEditable) { this.#editableAnnotations.set(element.data.id, element); @@ -4052,11 +4067,14 @@ class AnnotationLayer { * @param {AnnotationLayerParameters} viewport * @memberof AnnotationLayer */ - update({ viewport }) { + update({ viewport, optionalContentConfig }) { const layer = this.div; this.viewport = viewport; setLayerDimensions(layer, { rotation: viewport.rotation }); + for (const element of this.#elements) { + element.updateOC(optionalContentConfig); + } this.#setAnnotationCanvasMap(); layer.hidden = false; } diff --git a/test/driver.js b/test/driver.js index 7e098f7e05895..0ac1b7374111f 100644 --- a/test/driver.js +++ b/test/driver.js @@ -242,7 +242,8 @@ class Rasterize { fieldObjects, page, imageResourcesPath, - renderForms = false + renderForms = false, + optionalContentConfigPromise = null ) { try { const { svg, foreignObject, style, div } = this.createContainer(viewport); @@ -263,6 +264,7 @@ class Rasterize { imageResourcesPath, renderForms, fieldObjects, + optionalContentConfig: await optionalContentConfigPromise, }; // Ensure that the annotationLayer gets translated. @@ -1355,7 +1357,8 @@ class Driver { task.fieldObjects, page, IMAGE_RESOURCES_PATH, - renderForms + renderForms, + task.optionalContentConfigPromise ).then(() => { completeRender(false); }); diff --git a/test/pdfs/issue20433.pdf.link b/test/pdfs/issue20433.pdf.link new file mode 100644 index 0000000000000..91de8cc65b939 --- /dev/null +++ b/test/pdfs/issue20433.pdf.link @@ -0,0 +1 @@ +https://web.archive.org/web/20251107005559/https://octopdf.com/octopdf-sample.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index adc5211c63109..2f553d42b068d 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1956,6 +1956,42 @@ "rounds": 1, "type": "eq" }, + { + "id": "issue20433-initial", + "file": "pdfs/issue20433.pdf", + "md5": "3a550da7807540982ed457397667db79", + "link": true, + "rounds": 1, + "firstPage": 2, + "type": "eq", + "forms": true + }, + { + "id": "issue20433-no-form", + "file": "pdfs/issue20433.pdf", + "md5": "3a550da7807540982ed457397667db79", + "link": true, + "rounds": 1, + "firstPage": 2, + "type": "eq", + "forms": true, + "optionalContent": { + "73R": false + } + }, + { + "id": "issue20433-no-mathml", + "file": "pdfs/issue20433.pdf", + "md5": "3a550da7807540982ed457397667db79", + "link": true, + "rounds": 1, + "firstPage": 2, + "type": "eq", + "forms": true, + "optionalContent": { + "115R": false + } + }, { "id": "issue13845", "file": "pdfs/issue13845.pdf", diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 8c48a2bdbf4b0..2a30043ee3af1 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -63,6 +63,7 @@ import { PresentationModeState } from "./ui_utils.js"; * @property {PageViewport} viewport * @property {string} [intent] - The default value is "display". * @property {StructTreeLayerBuilder} [structTreeLayer] + * @property {Promise} [optionalContentConfigPromise] */ class AnnotationLayerBuilder { @@ -125,8 +126,15 @@ class AnnotationLayerBuilder { * @returns {Promise} A promise that is resolved when rendering of the * annotations is complete. */ - async render({ viewport, intent = "display", structTreeLayer = null }) { + async render({ + viewport, + intent = "display", + structTreeLayer = null, + optionalContentConfigPromise = null, + }) { if (this.div) { + const optionalContentConfig = await optionalContentConfigPromise; + if (this._cancelled || !this.annotationLayer) { return; } @@ -134,15 +142,18 @@ class AnnotationLayerBuilder { // transformation matrices. this.annotationLayer.update({ viewport: viewport.clone({ dontFlip: true }), + optionalContentConfig, }); return; } - const [annotations, hasJSActions, fieldObjects] = await Promise.all([ - this.pdfPage.getAnnotations({ intent }), - this._hasJSActionsPromise, - this._fieldObjectsPromise, - ]); + const [annotations, hasJSActions, fieldObjects, optionalContentConfig] = + await Promise.all([ + this.pdfPage.getAnnotations({ intent }), + this._hasJSActionsPromise, + this._fieldObjectsPromise, + optionalContentConfigPromise, + ]); if (this._cancelled) { return; } @@ -169,6 +180,7 @@ class AnnotationLayerBuilder { enableScripting: this.enableScripting, hasJSActions, fieldObjects, + optionalContentConfig, }); this.#annotations = annotations; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 05124fc8f4aba..070151b8ef92c 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -464,6 +464,7 @@ class PDFPageView extends BasePDFPageView { viewport: this.viewport, intent: "display", structTreeLayer: this.structTreeLayer, + optionalContentConfigPromise: this._optionalContentConfigPromise, }); } catch (ex) { console.error("#renderAnnotationLayer:", ex); From bd14524536c34d3899565d9b02fa9cffaaac1a55 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 23 May 2026 16:29:29 +0200 Subject: [PATCH 2/6] Shorten the `XfaLayerBuilder.prototype.render` method Given that the "print" intent is handled separately, there's currently a little bit of unnecessary code duplication in this method. --- web/xfa_layer_builder.js | 50 ++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/web/xfa_layer_builder.js b/web/xfa_layer_builder.js index 8cae48ad3dd96..3f74a7a9f0698 100644 --- a/web/xfa_layer_builder.js +++ b/web/xfa_layer_builder.js @@ -37,6 +37,10 @@ import { XfaLayer } from "pdfjs-lib"; */ class XfaLayerBuilder { + #cancelled = false; + + div = null; + /** * @param {XfaLayerBuilderOptions} options */ @@ -50,9 +54,6 @@ class XfaLayerBuilder { this.annotationStorage = annotationStorage; this.linkService = linkService; this.xfaHtml = xfaHtml; - - this.div = null; - this._cancelled = false; } /** @@ -62,50 +63,33 @@ class XfaLayerBuilder { * with a `textDivs` property that can be used with the TextHighlighter. */ async render({ viewport, intent = "display" }) { + let xfaHtml; if (intent === "print") { - const parameters = { - viewport: viewport.clone({ dontFlip: true }), - div: this.div, - xfaHtml: this.xfaHtml, - annotationStorage: this.annotationStorage, - linkService: this.linkService, - intent, - }; - - // Create an xfa layer div and render the form - this.div = document.createElement("div"); - parameters.div = this.div; - - return XfaLayer.render(parameters); - } + xfaHtml = this.xfaHtml; + } else { + xfaHtml = await this.pdfPage.getXfa(); - // intent === "display" - const xfaHtml = await this.pdfPage.getXfa(); - if (this._cancelled || !xfaHtml) { - return { textDivs: [] }; + if (this.#cancelled || !xfaHtml) { + return { textDivs: [] }; + } } - const parameters = { + // Create an xfa layer div and render the form + const hasDiv = !!this.div; + const params = { viewport: viewport.clone({ dontFlip: true }), - div: this.div, + div: (this.div ??= document.createElement("div")), xfaHtml, annotationStorage: this.annotationStorage, linkService: this.linkService, intent, }; - if (this.div) { - return XfaLayer.update(parameters); - } - // Create an xfa layer div and render the form - this.div = document.createElement("div"); - parameters.div = this.div; - - return XfaLayer.render(parameters); + return hasDiv ? XfaLayer.update(params) : XfaLayer.render(params); } cancel() { - this._cancelled = true; + this.#cancelled = true; } hide() { From 05de3c8a8855ab57f897f15847ec14ada9f0ded2 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 23 May 2026 22:40:14 +0200 Subject: [PATCH 3/6] Fix the `XRefWrapper` implementation, in the `src/core/editor/pdf_editor.js` file When comparing this code with the full `XRef` class it doesn't seem to be entirely correctly implemented, since the `fetch` method is basically doing what the `fetchIfRef` method is intended to do. --- src/core/editor/pdf_editor.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/core/editor/pdf_editor.js b/src/core/editor/pdf_editor.js index 9f360b7f20d3b..68254cd9926bf 100644 --- a/src/core/editor/pdf_editor.js +++ b/src/core/editor/pdf_editor.js @@ -87,24 +87,27 @@ class XRefWrapper { this._getNewRef = getNewRef; } - fetch(ref) { - return ref instanceof Ref ? this.entries[ref.num] : ref; + getNewTemporaryRef() { + return this._getNewRef(); } - fetchIfRefAsync(ref) { - return Promise.resolve(this.fetch(ref)); + fetchIfRef(obj) { + return obj instanceof Ref ? this.fetch(obj) : obj; } - fetchIfRef(ref) { - return this.fetch(ref); + fetch(ref) { + if (!(ref instanceof Ref)) { + throw new Error("ref object is not a reference"); + } + return this.entries[ref.num]; } - fetchAsync(ref) { - return Promise.resolve(this.fetch(ref)); + async fetchIfRefAsync(obj) { + return obj instanceof Ref ? this.fetchAsync(obj) : obj; } - getNewTemporaryRef() { - return this._getNewRef(); + async fetchAsync(ref) { + return this.fetch(ref); } } @@ -193,8 +196,7 @@ class PDFEditor { * @returns {Ref} */ get newRef() { - const ref = Ref.get(this.newRefCount++, 0); - return ref; + return Ref.get(this.newRefCount++, 0); } /** @@ -518,9 +520,9 @@ class PDFEditor { attributes = [attributes]; } for (let attr of attributes) { - attr = this.xrefWrapper.fetch(attr); + attr = this.xrefWrapper.fetchIfRef(attr); if (isName(attr.get("O"), "Table") && attr.has("Headers")) { - const headers = this.xrefWrapper.fetch(attr.getRaw("Headers")); + const headers = this.xrefWrapper.fetchIfRef(attr.getRaw("Headers")); if (Array.isArray(headers)) { for (let i = 0, ii = headers.length; i < ii; i++) { const newId = dedupIDs.get( From 31c6561b91056ebb5396637cc0e2566bfecd459c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 24 May 2026 10:19:34 +0200 Subject: [PATCH 4/6] Shorten the `fontFile` lookup a tiny bit Rather than effectively duplicating code, we can use a loop instead. --- src/core/evaluator.js | 15 ++++----------- src/core/fonts.js | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 4f33989d0757e..b99dfcca1ea9b 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -4666,18 +4666,11 @@ class PartialEvaluator { let fontFile, fontFileN, subtype, length1, length2, length3; try { - fontFile = descriptor.get("FontFile"); - if (fontFile) { - fontFileN = 1; - } else { - fontFile = descriptor.get("FontFile2"); + for (const n of ["FontFile", "FontFile2", "FontFile3"]) { + fontFile = descriptor.get(n); if (fontFile) { - fontFileN = 2; - } else { - fontFile = descriptor.get("FontFile3"); - if (fontFile) { - fontFileN = 3; - } + fontFileN = n; + break; } } diff --git a/src/core/fonts.js b/src/core/fonts.js index 8cf66e1311693..46528c316f55f 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -2698,7 +2698,7 @@ class Font { if ( (header.version === "OTTO" && (!properties.composite || - (properties.fontFileN === 3 && parsedCff?.isCIDFont))) || + (properties.fontFileN === "FontFile3" && parsedCff?.isCIDFont))) || !tables.head || !tables.hhea || !tables.maxp || From 057507f6ce886313ae79d52508734993bda4abd9 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 24 May 2026 10:56:51 +0200 Subject: [PATCH 5/6] Use `Uint8Array.fromBase64` in the `test/integration/highlight_editor_spec.mjs` file --- test/integration/highlight_editor_spec.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index ec4e7d130185f..d172b5ac52acd 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -2271,9 +2271,7 @@ describe("Highlight Editor", () => { const pdfData = fs.readFileSync(pdfPath).toString("base64"); const dataTransfer = await page.evaluateHandle(data => { const transfer = new DataTransfer(); - const view = Uint8Array.from(atob(data), code => - code.charCodeAt(0) - ); + const view = Uint8Array.fromBase64(data); const file = new File([view], "basicapi.pdf", { type: "application/pdf", }); From 59086fa5827cd03793c8235c03fc8cb14dc7b007 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 24 May 2026 12:20:18 +0200 Subject: [PATCH 6/6] Simplify how the character BBox is scaled in `src/display/canvas_dependency_tracker.js` In many/most PDF documents every glyph will require that the character BBox has scaling/offset applied, which can be made a tiny bit more efficient. In particular: - Avoid creating one additional temporary Array for every glyph. - Simplify the helper function, since there's no skew-components. --- src/display/canvas_dependency_tracker.js | 62 ++++++++---------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/src/display/canvas_dependency_tracker.js b/src/display/canvas_dependency_tracker.js index d448fa8c80f93..1cdefc715f489 100644 --- a/src/display/canvas_dependency_tracker.js +++ b/src/display/canvas_dependency_tracker.js @@ -28,54 +28,32 @@ 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) { +function scaleCharBBox(scaleX, scaleY, x, y, bbox) { let temp; - if (transform[0]) { - if (transform[0] < 0) { - temp = minMax[0]; - minMax[0] = minMax[2]; - minMax[2] = temp; + if (scaleX) { + if (scaleX < 0) { + temp = bbox[0]; + bbox[0] = bbox[2]; + bbox[2] = temp; } - minMax[0] *= transform[0]; - minMax[2] *= transform[0]; + bbox[0] *= scaleX; + bbox[2] *= scaleX; - if (transform[3] < 0) { - temp = minMax[1]; - minMax[1] = minMax[3]; - minMax[3] = temp; + if (scaleY < 0) { + temp = bbox[1]; + bbox[1] = bbox[3]; + bbox[3] = temp; } - minMax[1] *= transform[3]; - minMax[3] *= transform[3]; + bbox[1] *= scaleY; + bbox[3] *= scaleY; } 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]; + bbox.fill(0); } - minMax[0] += transform[4]; - minMax[1] += transform[5]; - minMax[2] += transform[4]; - minMax[3] += transform[5]; + bbox[0] += x; + bbox[1] += y; + bbox[2] += x; + bbox[3] += y; } // This is computed rathter than hard-coded to keep into @@ -663,7 +641,7 @@ class CanvasDependencyTracker { computedBBox = [0, 0, 0, 0]; Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox); if (scale !== 1 || x !== 0 || y !== 0) { - scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox); + scaleCharBBox(scale, -scale, x, y, computedBBox); } if (isBBoxTrustworthy) {