From 84b58668530e1b613c2c4a050c54b679042a5cc6 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Jan 2026 13:16:06 +0100 Subject: [PATCH 1/5] Reduce duplication in the `pickPlatformItem` helper function Also, tweak code/comment used when handling "GoToR" destinations. --- src/core/catalog.js | 12 +++++++----- src/core/file_spec.js | 22 +++++++--------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/core/catalog.js b/src/core/catalog.js index 3f73e6d918389..6e183d96408d4 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -1628,18 +1628,20 @@ class Catalog { /* xref = */ null, /* skipContent = */ true ); - const { rawFilename } = fs.serializable; - url = rawFilename; + ({ rawFilename: url } = fs.serializable); } else if (typeof urlDict === "string") { url = urlDict; + } else { + break; } // NOTE: the destination is relative to the *remote* document. const remoteDest = fetchRemoteDest(action); - if (remoteDest && typeof url === "string") { + if (remoteDest) { // NOTE: We don't use the `updateUrlHash` function here, since - // the `createValidAbsoluteUrl` function (see below) already - // handles parsing and validation of the final URL. + // the `createValidAbsoluteUrl` function (see below) already handles + // parsing/validation of the final URL and manual splitting also + // ensures that the `unsafeUrl` property will be available/correct. url = /* baseUrl = */ url.split("#", 1)[0] + "#" + remoteDest; } // The 'NewWindow' property, equal to `LinkTarget.BLANK`. diff --git a/src/core/file_spec.js b/src/core/file_spec.js index d331af04e93ce..6e544295d9b80 100644 --- a/src/core/file_spec.js +++ b/src/core/file_spec.js @@ -18,21 +18,13 @@ import { BaseStream } from "./base_stream.js"; import { Dict } from "./primitives.js"; function pickPlatformItem(dict) { - if (!(dict instanceof Dict)) { - return null; - } - // Look for the filename in this order: - // UF, F, Unix, Mac, DOS - if (dict.has("UF")) { - return dict.get("UF"); - } else if (dict.has("F")) { - return dict.get("F"); - } else if (dict.has("Unix")) { - return dict.get("Unix"); - } else if (dict.has("Mac")) { - return dict.get("Mac"); - } else if (dict.has("DOS")) { - return dict.get("DOS"); + if (dict instanceof Dict) { + // Look for the filename in this order: UF, F, Unix, Mac, DOS + for (const key of ["UF", "F", "Unix", "Mac", "DOS"]) { + if (dict.has(key)) { + return dict.get(key); + } + } } return null; } From 640a3106d5e254e7392eae0861c3f5e30cdf6276 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Jan 2026 13:16:29 +0100 Subject: [PATCH 2/5] Remove caching/shadowing from the `FileSpec` getters, and simplify the code Given that only the `FileSpec.prototype.serializable` getter is ever invoked from "outside" of the class, and only once per `FileSpec`-instance, the caching/shadowing isn't actually necessary. Furthermore the `_contentRef`-caching wasn't actually correct, since it ended up storing a `BaseStream`-instance and those should *generally* never be cached. (Since calling `BaseStream.prototype.getBytes()` more than once, without resetting the stream in between, will return an empty TypedArray after the first time.) --- src/core/annotation.js | 4 ++-- src/core/catalog.js | 8 ++------ src/core/file_spec.js | 46 ++++++++++++++++-------------------------- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index f2f642b5008d5..a0da4ae462ff3 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -5290,8 +5290,8 @@ class FileAttachmentAnnotation extends MarkupAnnotation { constructor(params) { super(params); - const { dict, xref } = params; - const file = new FileSpec(dict.get("FS"), xref); + const { dict } = params; + const file = new FileSpec(dict.get("FS")); this.data.annotationType = AnnotationType.FILEATTACHMENT; this.data.hasOwnCanvas = this.data.noRotate; diff --git a/src/core/catalog.js b/src/core/catalog.js index 6e183d96408d4..9ab10f281a0f5 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -1057,7 +1057,7 @@ class Catalog { if (obj instanceof Dict && obj.has("EmbeddedFiles")) { const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref); for (const [key, value] of nameTree.getAll()) { - const fs = new FileSpec(value, this.xref); + const fs = new FileSpec(value); attachments ??= Object.create(null); attachments[stringToPDFString(key, /* keepEscapeSequence = */ true)] = fs.serializable; @@ -1623,11 +1623,7 @@ class Catalog { case "GoToR": const urlDict = action.get("F"); if (urlDict instanceof Dict) { - const fs = new FileSpec( - urlDict, - /* xref = */ null, - /* skipContent = */ true - ); + const fs = new FileSpec(urlDict, /* skipContent = */ true); ({ rawFilename: url } = fs.serializable); } else if (typeof urlDict === "string") { url = urlDict; diff --git a/src/core/file_spec.js b/src/core/file_spec.js index 6e544295d9b80..198a663c9bf07 100644 --- a/src/core/file_spec.js +++ b/src/core/file_spec.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { shadow, stringToPDFString, warn } from "../shared/util.js"; +import { stringToPDFString, warn } from "../shared/util.js"; import { BaseStream } from "./base_stream.js"; import { Dict } from "./primitives.js"; @@ -43,11 +43,10 @@ function stripPath(str) { class FileSpec { #contentAvailable = false; - constructor(root, xref, skipContent = false) { + constructor(root, skipContent = false) { if (!(root instanceof Dict)) { return; } - this.xref = xref; this.root = root; if (root.has("FS")) { this.fs = root.get("FS"); @@ -65,56 +64,45 @@ class FileSpec { } get filename() { - let filename = ""; - const item = pickPlatformItem(this.root); + let name; if (item && typeof item === "string") { - filename = stringToPDFString(item, /* keepEscapeSequence = */ true) + name = stringToPDFString(item, /* keepEscapeSequence = */ true) .replaceAll("\\\\", "\\") .replaceAll("\\/", "/") .replaceAll("\\", "/"); } - return shadow(this, "filename", filename || "unnamed"); + return name || "unnamed"; } get content() { if (!this.#contentAvailable) { return null; } - this._contentRef ||= pickPlatformItem(this.root?.get("EF")); + const ef = pickPlatformItem(this.root?.get("EF")); - let content = null; - if (this._contentRef) { - const fileObj = this.xref.fetchIfRef(this._contentRef); - if (fileObj instanceof BaseStream) { - content = fileObj.getBytes(); - } else { - warn( - "Embedded file specification points to non-existing/invalid content" - ); - } - } else { - warn("Embedded file specification does not have any content"); + if (ef instanceof BaseStream) { + return ef.getBytes(); } - return content; + warn("Embedded file specification points to non-existing/invalid content"); + return null; } get description() { - let description = ""; - const desc = this.root?.get("Desc"); if (desc && typeof desc === "string") { - description = stringToPDFString(desc); + return stringToPDFString(desc); } - return shadow(this, "description", description); + return ""; } get serializable() { + const { filename, content, description } = this; return { - rawFilename: this.filename, - filename: stripPath(this.filename), - content: this.content, - description: this.description, + rawFilename: filename, + filename: stripPath(filename), + content, + description, }; } } From ee26fcb16c722355ee873f3ce5eab265ed694cf8 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Jan 2026 13:16:48 +0100 Subject: [PATCH 3/5] Return the `rawFilename` as-is even if it's empty, from `FileSpec.prototype.serializable` It's more correct to return the `rawFilename` as-is, and limit the fallback for empty filenames to only the `filename` property. --- src/core/file_spec.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/file_spec.js b/src/core/file_spec.js index 198a663c9bf07..c52ff54d33a45 100644 --- a/src/core/file_spec.js +++ b/src/core/file_spec.js @@ -65,14 +65,13 @@ class FileSpec { get filename() { const item = pickPlatformItem(this.root); - let name; if (item && typeof item === "string") { - name = stringToPDFString(item, /* keepEscapeSequence = */ true) + return stringToPDFString(item, /* keepEscapeSequence = */ true) .replaceAll("\\\\", "\\") .replaceAll("\\/", "/") .replaceAll("\\", "/"); } - return name || "unnamed"; + return ""; } get content() { @@ -100,7 +99,7 @@ class FileSpec { const { filename, content, description } = this; return { rawFilename: filename, - filename: stripPath(filename), + filename: stripPath(filename) || "unnamed", content, description, }; From adf2c5bbd6762d2f6b3a765cb8c11a14b9a0d2e2 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Jan 2026 13:22:52 +0100 Subject: [PATCH 4/5] Remove the `Promise.try()` polyfill (During rebasing of the previous patches I happened to look at the polyfills and noticed that this one could be removed now.) Note: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try#browser_compatibility - https://bugzilla.mozilla.org/show_bug.cgi?id=1928493 --- src/shared/util.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/shared/util.js b/src/shared/util.js index b50819976f854..0ce6314350b88 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1260,19 +1260,6 @@ function fromBase64Util(str) { return stringToBytes(atob(str)); } -// TODO: Remove this once https://bugzilla.mozilla.org/show_bug.cgi?id=1928493 -// is fixed. -if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("SKIP_BABEL")) && - typeof Promise.try !== "function" -) { - Promise.try = function (fn, ...args) { - return new Promise(resolve => { - resolve(fn(...args)); - }); - }; -} - // TODO: Remove this once the `javascript.options.experimental.math_sumprecise` // preference is removed from Firefox. if (typeof Math.sumPrecise !== "function") { From 6e4624636fe52ee532a18c974a2128ebc084d29b Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Jan 2026 13:32:11 +0100 Subject: [PATCH 5/5] Remove the `Uint8Array.prototype.toHex()`, `Uint8Array.prototype.toBase64()`, and `Uint8Array.fromBase64()` polyfills (During rebasing of the previous patches I happened to look at the polyfills and noticed that this one could be removed now.) See: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toHex#browser_compatibility - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64#browser_compatibility - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64#browser_compatibility Note that technically this functionality can still be disabled via a preference in Firefox, however that's slated for removal in [bug 1985120](https://bugzilla.mozilla.org/show_bug.cgi?id=1985120). Looking at the Firefox source-code, see https://searchfox.org/firefox-main/search?q=array.tobase64%28%29&path=&case=false®exp=false, you can see that it's already being used *unconditionally* elsewhere in the browser hence removing the polyfills ought to be fine (since toggling the preference would break other parts of the browser). --- src/core/document.js | 5 ++-- src/core/xfa/template.js | 4 +-- src/display/editor/drawers/signaturedraw.js | 6 ++--- src/display/font_loader.js | 3 +-- src/shared/util.js | 28 --------------------- test/unit/display_utils_spec.js | 6 ++--- 6 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index a33d20385c09c..80e49ca9f0aed 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -27,7 +27,6 @@ import { stringToBytes, stringToPDFString, stringToUTF8String, - toHexUtil, unreachable, Util, warn, @@ -1605,8 +1604,8 @@ class PDFDocument { } return shadow(this, "fingerprints", [ - toHexUtil(hashOriginal), - hashModified ? toHexUtil(hashModified) : null, + hashOriginal.toHex(), + hashModified?.toHex() ?? null, ]); } diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index 61322dcd9cda7..4c643d51722a5 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -90,7 +90,6 @@ import { XFAObject, XFAObjectArray, } from "./xfa_object.js"; -import { fromBase64Util, Util, warn } from "../../shared/util.js"; import { getBBox, getColor, @@ -103,6 +102,7 @@ import { getStringOption, HTMLResult, } from "./utils.js"; +import { Util, warn } from "../../shared/util.js"; import { getMetrics } from "./fonts.js"; import { recoverJsURL } from "../core_utils.js"; import { searchNode } from "./som.js"; @@ -3420,7 +3420,7 @@ class Image extends StringObject { } if (!buffer && this.transferEncoding === "base64") { - buffer = fromBase64Util(this[$content]); + buffer = Uint8Array.fromBase64(this[$content]); } if (!buffer) { diff --git a/src/display/editor/drawers/signaturedraw.js b/src/display/editor/drawers/signaturedraw.js index f90a1428edac2..d3e217d6cda7a 100644 --- a/src/display/editor/drawers/signaturedraw.js +++ b/src/display/editor/drawers/signaturedraw.js @@ -13,10 +13,10 @@ * limitations under the License. */ -import { fromBase64Util, toBase64Util, warn } from "../../../shared/util.js"; import { ContourDrawOutline } from "./contour.js"; import { InkDrawOutline } from "./inkdraw.js"; import { Outline } from "./outline.js"; +import { warn } from "../../../shared/util.js"; const BASE_HEADER_LENGTH = 8; const POINTS_PROPERTIES_NUMBER = 3; @@ -749,12 +749,12 @@ class SignatureExtractor { const buf = await new Response(cs.readable).arrayBuffer(); const bytes = new Uint8Array(buf); - return toBase64Util(bytes); + return bytes.toBase64(); } static async decompressSignature(signatureData) { try { - const bytes = fromBase64Util(signatureData); + const bytes = Uint8Array.fromBase64(signatureData); const { readable, writable } = new DecompressionStream("deflate-raw"); const writer = writable.getWriter(); await writer.ready; diff --git a/src/display/font_loader.js b/src/display/font_loader.js index b74349ac0f6fc..b7b27e6ddbfd8 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -19,7 +19,6 @@ import { isNodeJS, shadow, string32, - toBase64Util, unreachable, warn, } from "../shared/util.js"; @@ -408,7 +407,7 @@ class FontFaceObject { return null; } // Add the @font-face rule to the document. - const url = `url(data:${this.mimetype};base64,${toBase64Util(this.data)});`; + const url = `url(data:${this.mimetype};base64,${this.data.toBase64()});`; let rule; if (!this.cssFontInfo) { rule = `@font-face {font-family:"${this.loadedName}";src:${url}}`; diff --git a/src/shared/util.js b/src/shared/util.js index 0ce6314350b88..df9138133e43d 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1235,31 +1235,6 @@ function MathClamp(v, min, max) { return Math.min(Math.max(v, min), max); } -// TODO: Remove this once `Uint8Array.prototype.toHex` is generally available. -function toHexUtil(arr) { - if (Uint8Array.prototype.toHex) { - return arr.toHex(); - } - return Array.from(arr, num => hexNumbers[num]).join(""); -} - -// TODO: Remove this once `Uint8Array.prototype.toBase64` is generally -// available. -function toBase64Util(arr) { - if (Uint8Array.prototype.toBase64) { - return arr.toBase64(); - } - return btoa(bytesToString(arr)); -} - -// TODO: Remove this once `Uint8Array.fromBase64` is generally available. -function fromBase64Util(str) { - if (Uint8Array.fromBase64) { - return Uint8Array.fromBase64(str); - } - return stringToBytes(atob(str)); -} - // TODO: Remove this once the `javascript.options.experimental.math_sumprecise` // preference is removed from Firefox. if (typeof Math.sumPrecise !== "function") { @@ -1325,7 +1300,6 @@ export { FeatureTest, FONT_IDENTITY_MATRIX, FormatError, - fromBase64Util, getModificationDate, getUuid, getVerbosityLevel, @@ -1355,8 +1329,6 @@ export { stringToPDFString, stringToUTF8String, TextRenderingMode, - toBase64Util, - toHexUtil, UnknownErrorException, unreachable, updateUrlHash, diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index 45a0d16149c68..87da7c0ca063d 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -22,7 +22,7 @@ import { PDFDateString, renderRichText, } from "../../src/display/display_utils.js"; -import { isNodeJS, toBase64Util } from "../../src/shared/util.js"; +import { isNodeJS } from "../../src/shared/util.js"; describe("display_utils", function () { describe("getFilenameFromUrl", function () { @@ -183,9 +183,7 @@ describe("display_utils", function () { it('gets fallback filename from query string appended to "data:" URL', function () { const typedArray = new Uint8Array([1, 2, 3, 4, 5]); - const dataUrl = `data:application/pdf;base64,${toBase64Util(typedArray)}`; - // Sanity check to ensure that a "data:" URL was returned. - expect(dataUrl.startsWith("data:")).toEqual(true); + const dataUrl = `data:application/pdf;base64,${typedArray.toBase64()}`; expect(getPdfFilenameFromUrl(dataUrl + "?file1.pdf")).toEqual( "document.pdf"