From fbf36b603f381d203f966df6b871cc7dbb6f3bf9 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 22 Aug 2026 12:21:14 +0200 Subject: [PATCH 1/5] Modernize the Node.js `getinfo.mjs` example - Fix metadata logging, since it's accidentally broken by PR 19778 (over a year ago). - Modernize, and simplify, the example by using `await` to remove the promise chains. --- examples/node/getinfo.mjs | 94 ++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/examples/node/getinfo.mjs b/examples/node/getinfo.mjs index f3918f9acc873..2db6fcb639556 100644 --- a/examples/node/getinfo.mjs +++ b/examples/node/getinfo.mjs @@ -15,62 +15,44 @@ const pdfPath = // Will be using promises to load document, pages and misc data instead of // callback. const loadingTask = getDocument({ url: pdfPath }); -loadingTask.promise - .then(function (doc) { - const numPages = doc.numPages; - console.log("# Document Loaded"); - console.log("Number of Pages: " + numPages); +try { + const pdfDoc = await loadingTask.promise; + + const { numPages } = pdfDoc; + console.log("# Document Loaded"); + console.log(`Number of Pages: ${numPages}`); + console.log(); + + const { info, metadata } = await pdfDoc.getMetadata(); + console.log("# Metadata is Loaded"); + console.log("## Info"); + console.log(JSON.stringify(info, null, 2)); + console.log(); + if (metadata) { + console.log("## Metadata"); + console.log(JSON.stringify(Object.fromEntries(metadata), null, 2)); console.log(); + } - let lastPromise; // will be used to chain promises - lastPromise = doc.getMetadata().then(function (data) { - console.log("# Metadata Is Loaded"); - console.log("## Info"); - console.log(JSON.stringify(data.info, null, 2)); - console.log(); - if (data.metadata) { - console.log("## Metadata"); - console.log(JSON.stringify(data.metadata.getAll(), null, 2)); - console.log(); - } - }); + for (let i = 1; i <= numPages; i++) { + const pdfPage = await pdfDoc.getPage(i); + console.log(`# Page ${i}`); + const viewport = pdfPage.getViewport({ scale: 1.0 }); + console.log(`Size: ${viewport.width}x${viewport.height}`); + console.log(); + + const { items } = await pdfPage.getTextContent(); + // Content contains lots of information about the text layout and + // styles, but we need only strings at the moment + console.log("## Text Content"); + console.log(items.map(item => item.str).join(" ")); + console.log(); + // Release page resources. + pdfPage.cleanup(); + } - const loadPage = function (pageNum) { - return doc.getPage(pageNum).then(function (page) { - console.log("# Page " + pageNum); - const viewport = page.getViewport({ scale: 1.0 }); - console.log("Size: " + viewport.width + "x" + viewport.height); - console.log(); - return page - .getTextContent() - .then(function (content) { - // Content contains lots of information about the text layout and - // styles, but we need only strings at the moment - const strings = content.items.map(function (item) { - return item.str; - }); - console.log("## Text Content"); - console.log(strings.join(" ")); - // Release page resources. - page.cleanup(); - }) - .then(function () { - console.log(); - }); - }); - }; - // Loading of the first page will wait on metadata and subsequent loadings - // will wait on the previous pages. - for (let i = 1; i <= numPages; i++) { - lastPromise = lastPromise.then(loadPage.bind(null, i)); - } - return lastPromise; - }) - .then( - function () { - console.log("# End of Document"); - }, - function (err) { - console.error("Error: " + err); - } - ); + await loadingTask.destroy(); + console.log("# End of Document"); +} catch (ex) { + console.error(`Error: ${ex}`); +} From 799a57e0fb75dd37432638be78d2885535247bfb Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 22 Aug 2026 15:39:15 +0200 Subject: [PATCH 2/5] Fix inconsistencies with the `Page.prototype.#replaceIdByRef` method This private method has two call-sites, which provide *different* `deletedAnnotations` parameters; see - https://github.com/mozilla/pdf.js/blob/0f26334f9d6f96119f6e5164fb65832fbbde7344/src/core/document.js#L378-L384 - https://github.com/mozilla/pdf.js/blob/0f26334f9d6f96119f6e5164fb65832fbbde7344/src/core/document.js#L534-L539 Thanks to the similarities between the `RefMap` and `RefSet` classes this inconsistency hasn't caused any bugs, as far as I know, but it should still be fixed. Given how the `deletedAnnotations` is being used, a `RefSet` really seems to be the "correct" data-structure to use here since we only need to track references. Finally, make use of an early `continue` to reduce overall indentation and thus shorten the code in the `Page.prototype.#replaceIdByRef` method. --- src/core/document.js | 69 ++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index 7e1adf0a68316..53a238cabb424 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -327,44 +327,45 @@ class Page { async #replaceIdByRef(annotations, deletedAnnotations, existingAnnotations) { const promises = []; for (const annotation of annotations) { - if (annotation.id) { - const ref = Ref.fromString(annotation.id); - if (!ref) { - warn(`A non-linked annotation cannot be modified: ${annotation.id}`); - continue; - } - if (annotation.deleted) { - deletedAnnotations.put(ref, ref); - if (annotation.popupRef) { - const popupRef = Ref.fromString(annotation.popupRef); - if (popupRef) { - deletedAnnotations.put(popupRef, popupRef); - } - } - continue; - } - if (annotation.popup?.deleted) { + if (!annotation.id) { + continue; + } + const ref = Ref.fromString(annotation.id); + if (!ref) { + warn(`A non-linked annotation cannot be modified: ${annotation.id}`); + continue; + } + if (annotation.deleted) { + deletedAnnotations.put(ref); + if (annotation.popupRef) { const popupRef = Ref.fromString(annotation.popupRef); if (popupRef) { - deletedAnnotations.put(popupRef, popupRef); + deletedAnnotations.put(popupRef); } } - existingAnnotations?.put(ref); - annotation.ref = ref; - promises.push( - this.xref.fetchAsync(ref).then( - obj => { - if (obj instanceof Dict) { - annotation.oldAnnotation = obj.clone(); - } - }, - () => { - warn(`Cannot fetch \`oldAnnotation\` for: ${ref}.`); - } - ) - ); - delete annotation.id; + continue; + } + if (annotation.popup?.deleted) { + const popupRef = Ref.fromString(annotation.popupRef); + if (popupRef) { + deletedAnnotations.put(popupRef); + } } + existingAnnotations?.put(ref); + annotation.ref = ref; + promises.push( + this.xref.fetchAsync(ref).then( + obj => { + if (obj instanceof Dict) { + annotation.oldAnnotation = obj.clone(); + } + }, + () => { + warn(`Cannot fetch \`oldAnnotation\` for: ${ref}.`); + } + ) + ); + delete annotation.id; } await Promise.all(promises); } @@ -375,7 +376,7 @@ class Page { } const partialEvaluator = this.#createPartialEvaluator(handler); - const deletedAnnotations = new RefMap(); + const deletedAnnotations = new RefSet(); const existingAnnotations = new RefSet(); await this.#replaceIdByRef( annotations, From 3451fc5037a5fddf2f686cd67924efb9acc7a596 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sat, 22 Aug 2026 22:02:54 +0200 Subject: [PATCH 3/5] Shorten the handling of /A and /a page labels I happened to glance at this code, and noticed that using the style-value *itself* to compute the current character would shorten this code a little bit. Note that this particular page label format seems to be somewhat rarely used in practice, compared to e.g. the roman numerals format, which probably isn't that strange given its definition: https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#G11.2096063 --- src/core/catalog.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core/catalog.js b/src/core/catalog.js index 028e1b5bfbb01..a615317d4303b 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -918,13 +918,9 @@ class Catalog { case "A": case "a": const LIMIT = 26; // Use only the characters A-Z, or a-z. - const A_UPPER_CASE = 0x41, - A_LOWER_CASE = 0x61; - - const baseCharCode = style === "a" ? A_LOWER_CASE : A_UPPER_CASE; const letterIndex = currentIndex - 1; const character = String.fromCharCode( - baseCharCode + (letterIndex % LIMIT) + style.charCodeAt(0) + (letterIndex % LIMIT) ); currentLabel = character.repeat(Math.floor(letterIndex / LIMIT) + 1); break; From 2ae59958a73ab3c06691c3df9804c334ab9a7f1b Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 23 Aug 2026 11:51:57 +0200 Subject: [PATCH 4/5] Remove the unused `key3` parameter from the various `Dict` get-methods PR 20542 removed the only spot in the code-base where a `Dict` get-method was invoked with three keys, hence we can remove a little bit of effectively dead code. Note that for any "regular" `Dict` lookup there can be at most two keys, since some PDF properties have shorthand names (e.g. /CS respectively /ColorSpace). --- src/core/primitives.js | 23 ++++++------------- test/unit/primitives_spec.js | 44 +++++++++++------------------------- 2 files changed, 20 insertions(+), 47 deletions(-) diff --git a/src/core/primitives.js b/src/core/primitives.js index 015d244030d2f..df2f3357c44f1 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -89,7 +89,7 @@ class Dict { return this.#map.size; } - #getValue(isAsync, key1, key2, key3) { + #getValue(isAsync, key1, key2) { let value = this.#map.get(key1); if (value === undefined && key2 !== undefined) { if ( @@ -99,15 +99,6 @@ class Dict { unreachable("Dict.#getValue: Expected keys to be ordered by length."); } value = this.#map.get(key2); - if (value === undefined && key3 !== undefined) { - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) && - key3.length < key2.length - ) { - unreachable("Dict.#getValue: Expected keys to be ordered by length."); - } - value = this.#map.get(key3); - } } if (value instanceof Ref && this.xref) { return isAsync @@ -118,18 +109,18 @@ class Dict { } // Automatically dereferences Ref objects. - get(key1, key2, key3) { - return this.#getValue(/* isAsync = */ false, key1, key2, key3); + get(key1, key2) { + return this.#getValue(/* isAsync = */ false, key1, key2); } // Same as get(), but returns a promise and uses fetchIfRefAsync(). - async getAsync(key1, key2, key3) { - return this.#getValue(/* isAsync = */ true, key1, key2, key3); + async getAsync(key1, key2) { + return this.#getValue(/* isAsync = */ true, key1, key2); } // Same as get(), but dereferences all elements if the result is an Array. - getArray(key1, key2, key3) { - let value = this.#getValue(/* isAsync = */ false, key1, key2, key3); + getArray(key1, key2) { + let value = this.#getValue(/* isAsync = */ false, key1, key2); if (Array.isArray(value)) { value = value.slice(); // Ensure that we don't modify the Dict data. diff --git a/test/unit/primitives_spec.js b/test/unit/primitives_spec.js index fb31e53f08da1..096c7bd7e6bd9 100644 --- a/test/unit/primitives_spec.js +++ b/test/unit/primitives_spec.js @@ -98,14 +98,13 @@ describe("primitives", function () { expect(dict.get()).toBeUndefined(); expect(dict.get("Prev")).toBeUndefined(); expect(dict.get("D", "Decode")).toBeUndefined(); - expect(dict.get("FontFile", "FontFile2", "FontFile3")).toBeUndefined(); + expect(dict.get("FontFile", "FontFile2")).toBeUndefined(); }; let emptyDict, dictWithSizeKey, dictWithManyKeys; const storedSize = 42; const testFontFile = "file1"; const testFontFile2 = "file2"; - const testFontFile3 = "file3"; beforeAll(function () { emptyDict = new Dict(); @@ -116,7 +115,6 @@ describe("primitives", function () { dictWithManyKeys = new Dict(); dictWithManyKeys.set("FontFile", testFontFile); dictWithManyKeys.set("FontFile2", testFontFile2); - dictWithManyKeys.set("FontFile3", testFontFile3); }); afterAll(function () { @@ -153,7 +151,6 @@ describe("primitives", function () { expect(dictWithSizeKey.get("Size")).toEqual(storedSize); expect(dictWithSizeKey.get("Prev", "Size")).toEqual(storedSize); - expect(dictWithSizeKey.get("Prev", "Root", "Size")).toEqual(storedSize); }); it("should return invalid values for unknown keys when Size key is stored", function () { @@ -186,21 +183,16 @@ describe("primitives", function () { it("should return correct values for multiple stored keys", function () { expect(dictWithManyKeys.has("FontFile")).toBeTrue(); expect(dictWithManyKeys.has("FontFile2")).toBeTrue(); - expect(dictWithManyKeys.has("FontFile3")).toBeTrue(); - expect(dictWithManyKeys.get("FontFile3")).toEqual(testFontFile3); - expect(dictWithManyKeys.get("FontFile2", "FontFile3")).toEqual( - testFontFile2 + expect(dictWithManyKeys.get("FontFile", "FontFile2")).toEqual( + testFontFile ); - expect( - dictWithManyKeys.get("FontFile", "FontFile2", "FontFile3") - ).toEqual(testFontFile); }); it("should asynchronously fetch unknown keys", async function () { const keyPromises = [ dictWithManyKeys.getAsync("Size"), - dictWithSizeKey.getAsync("FontFile", "FontFile2", "FontFile3"), + dictWithSizeKey.getAsync("FontFile", "FontFile2"), ]; const values = await Promise.all(keyPromises); @@ -210,22 +202,19 @@ describe("primitives", function () { it("should asynchronously fetch correct values for multiple stored keys", async function () { const keyPromises = [ - dictWithManyKeys.getAsync("FontFile3"), - dictWithManyKeys.getAsync("FontFile2", "FontFile3"), - dictWithManyKeys.getAsync("FontFile", "FontFile2", "FontFile3"), + dictWithManyKeys.getAsync("FontFile2"), + dictWithManyKeys.getAsync("FontFile", "FontFile2"), ]; const values = await Promise.all(keyPromises); - expect(values[0]).toEqual(testFontFile3); - expect(values[1]).toEqual(testFontFile2); - expect(values[2]).toEqual(testFontFile); + expect(values[0]).toEqual(testFontFile2); + expect(values[1]).toEqual(testFontFile); }); it("should iterate through each stored key", function () { expect([...dictWithManyKeys]).toEqual([ ["FontFile", testFontFile], ["FontFile2", testFontFile2], - ["FontFile3", testFontFile3], ]); }); @@ -236,15 +225,9 @@ describe("primitives", function () { fontDict.set("FontFile", fontRef); expect(fontDict.getRaw("FontFile")).toEqual(fontRef); - expect(fontDict.get("FontFile", "FontFile2", "FontFile3")).toEqual( - testFontFile - ); + expect(fontDict.get("FontFile", "FontFile2")).toEqual(testFontFile); - const value = await fontDict.getAsync( - "FontFile", - "FontFile2", - "FontFile3" - ); + const value = await fontDict.getAsync("FontFile", "FontFile2"); expect(value).toEqual(testFontFile); }); @@ -275,7 +258,7 @@ describe("primitives", function () { }); it("should get all key names", function () { - const expectedKeys = ["FontFile", "FontFile2", "FontFile3"]; + const expectedKeys = ["FontFile", "FontFile2"]; const keys = [...dictWithManyKeys.getKeys()]; expect(keys.sort()).toEqual(expectedKeys); @@ -283,7 +266,7 @@ describe("primitives", function () { it("should get all raw values", function () { // Test direct objects: - const expectedRawValues1 = [testFontFile, testFontFile2, testFontFile3]; + const expectedRawValues1 = [testFontFile, testFontFile2]; const rawValues1 = [...dictWithManyKeys.getRawValues()]; expect(rawValues1.sort()).toEqual(expectedRawValues1); @@ -314,7 +297,6 @@ describe("primitives", function () { const expectedRawEntries = [ ["FontFile", testFontFile], ["FontFile2", testFontFile2], - ["FontFile3", testFontFile3], ]; const rawEntries = Array.from(dictWithManyKeys.getRawEntries()); expect(rawEntries.sort()).toEqual(expectedRawEntries); @@ -329,7 +311,7 @@ describe("primitives", function () { }); it("should correctly merge dictionaries", function () { - const expectedKeys = ["FontFile", "FontFile2", "FontFile3", "Size"]; + const expectedKeys = ["FontFile", "FontFile2", "Size"]; const fontFileDict = new Dict(); fontFileDict.set("FontFile", "Type1 font file"); From 8a3c8348b0a5192aad2473fe2587df2981ae1cd7 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 23 Aug 2026 13:38:33 +0200 Subject: [PATCH 5/5] Add a helper method, in the `PartialEvaluator` class, for building a transfer map Currently we duplicate the same exact code twice, which seems unnecessary. --- src/core/evaluator.js | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 96e2d5d81a570..aa35caa8f9f67 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -885,6 +885,16 @@ class PartialEvaluator { } } + #createTransferMap(fn) { + const transferFn = this._pdfFunctionFactory.create(fn), + tmp = new Float32Array(1); + return Uint8Array.from({ length: 256 }, (_, i) => { + tmp[0] = i / 255; + transferFn(tmp, 0, tmp, 0); + return (tmp[0] * 255) | 0; + }); + } + handleSMask( smask, resources, @@ -904,15 +914,7 @@ class PartialEvaluator { // we will build a map of integer values in range 0..255 to be fast. const transferObj = smask.get("TR"); if (isPDFFunction(transferObj)) { - const transferFn = this._pdfFunctionFactory.create(transferObj); - const transferMap = new Uint8Array(256); - const tmp = new Float32Array(1); - for (let i = 0; i < 256; i++) { - tmp[0] = i / 255; - transferFn(tmp, 0, tmp, 0); - transferMap[i] = (tmp[0] * 255) | 0; - } - smaskOptions.transferMap = transferMap; + smaskOptions.transferMap = this.#createTransferMap(transferObj); } return this.buildFormXObject( @@ -930,12 +932,9 @@ class PartialEvaluator { handleTransferFunction(tr) { let transferArray; if (Array.isArray(tr)) { - transferArray = tr; - if (tr.length > 1 && tr.every(map => map === tr[0])) { - // All entries in the array are the same, so we can just use one of - // them. - transferArray = [tr[0]]; - } + // If all entries in the array are the same, we can just use one of them. + transferArray = + tr.length > 1 && tr.every(map => map === tr[0]) ? [tr[0]] : tr; } else if (isPDFFunction(tr)) { transferArray = [tr]; } else { @@ -955,16 +954,7 @@ class PartialEvaluator { } else if (!isPDFFunction(transferObj)) { return null; // Not a valid transfer function object. } - - const transferFn = this._pdfFunctionFactory.create(transferObj); - const transferMap = new Uint8Array(256), - tmp = new Float32Array(1); - for (let j = 0; j < 256; j++) { - tmp[0] = j / 255; - transferFn(tmp, 0, tmp, 0); - transferMap[j] = (tmp[0] * 255) | 0; - } - transferMaps.push(transferMap); + transferMaps.push(this.#createTransferMap(transferObj)); numEffectfulFns++; }