Merge pull request #20596 from Snuffleupagus/FileSpec-fixes

Simplify the `FileSpec` class, and remove no longer needed polyfills
This commit is contained in:
Tim van der Meij 2026-01-29 22:03:38 +01:00 committed by GitHub
commit 471adfd023
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 59 additions and 137 deletions

View File

@ -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;

View File

@ -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,23 +1623,21 @@ class Catalog {
case "GoToR":
const urlDict = action.get("F");
if (urlDict instanceof Dict) {
const fs = new FileSpec(
urlDict,
/* xref = */ null,
/* skipContent = */ true
);
const { rawFilename } = fs.serializable;
url = rawFilename;
const fs = new FileSpec(urlDict, /* skipContent = */ true);
({ 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`.

View File

@ -27,7 +27,6 @@ import {
stringToBytes,
stringToPDFString,
stringToUTF8String,
toHexUtil,
unreachable,
Util,
warn,
@ -1628,8 +1627,8 @@ class PDFDocument {
}
return shadow(this, "fingerprints", [
toHexUtil(hashOriginal),
hashModified ? toHexUtil(hashModified) : null,
hashOriginal.toHex(),
hashModified?.toHex() ?? null,
]);
}

View File

@ -13,26 +13,18 @@
* 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";
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;
}
@ -51,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");
@ -73,56 +64,46 @@ class FileSpec {
}
get filename() {
let filename = "";
const item = pickPlatformItem(this.root);
if (item && typeof item === "string") {
filename = stringToPDFString(item, /* keepEscapeSequence = */ true)
// NOTE: The following replacement order is INTENTIONAL, regardless of
// what some static code analysers (e.g. CodeQL) may claim.
return stringToPDFString(item, /* keepEscapeSequence = */ true)
.replaceAll("\\\\", "\\")
.replaceAll("\\/", "/")
.replaceAll("\\", "/");
}
return shadow(this, "filename", filename || "unnamed");
return "";
}
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) || "unnamed",
content,
description,
};
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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}}`;

View File

@ -1235,44 +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 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") {
@ -1338,7 +1300,6 @@ export {
FeatureTest,
FONT_IDENTITY_MATRIX,
FormatError,
fromBase64Util,
getModificationDate,
getUuid,
getVerbosityLevel,
@ -1368,8 +1329,6 @@ export {
stringToPDFString,
stringToUTF8String,
TextRenderingMode,
toBase64Util,
toHexUtil,
UnknownErrorException,
unreachable,
updateUrlHash,

View File

@ -1,7 +1,7 @@
import { decodeFontData, ttx, verifyTtxOutput } from "./fontutils.js";
import { ttx, verifyTtxOutput } from "./fontutils.js";
describe("font1", function () {
const font1_1 = decodeFontData(
const font1_1 = Uint8Array.fromBase64(
// eslint-disable-next-line max-len
"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"
);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,22 +14,10 @@
* limitations under the License.
*/
import { bytesToString, stringToBytes } from "../../src/shared/util.js";
function decodeFontData(base64) {
const str = atob(base64);
return stringToBytes(str);
}
function encodeFontData(data) {
const str = bytesToString(data);
return btoa(str);
}
async function ttx(data) {
const response = await fetch("/ttx", {
method: "POST",
body: encodeFontData(data),
body: data.toBase64(),
});
if (!response.ok) {
@ -45,4 +33,4 @@ function verifyTtxOutput(output) {
}
}
export { decodeFontData, encodeFontData, ttx, verifyTtxOutput };
export { ttx, verifyTtxOutput };

View File

@ -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"