[api-minor] Improve the FileSpec implementation

- Check that the `filename` is actually a string, before parsing it further.
 - Use proper "shadowing" in the `filename` getter.
 - Add a bit more validation of the data in `pickPlatformItem`.
 - Last, but not least, return both the original `filename` and the (path stripped) variant needed in the display-layer and viewer.
This commit is contained in:
Jonas Jenwald 2024-05-01 12:28:33 +02:00
parent 16dbf5dcfd
commit 2b69fb76ac
6 changed files with 47 additions and 46 deletions

View file

@ -1619,8 +1619,8 @@ class Catalog {
/* xref = */ null, /* xref = */ null,
/* skipContent = */ true /* skipContent = */ true
); );
const { filename } = fs.serializable; const { rawFilename } = fs.serializable;
url = filename; url = rawFilename;
} else if (typeof urlDict === "string") { } else if (typeof urlDict === "string") {
url = urlDict; url = urlDict;
} }

View file

@ -18,6 +18,9 @@ import { BaseStream } from "./base_stream.js";
import { Dict } from "./primitives.js"; import { Dict } from "./primitives.js";
function pickPlatformItem(dict) { function pickPlatformItem(dict) {
if (!(dict instanceof Dict)) {
return null;
}
// Look for the filename in this order: // Look for the filename in this order:
// UF, F, Unix, Mac, DOS // UF, F, Unix, Mac, DOS
if (dict.has("UF")) { if (dict.has("UF")) {
@ -34,6 +37,10 @@ function pickPlatformItem(dict) {
return null; return null;
} }
function stripPath(str) {
return str.substring(str.lastIndexOf("/") + 1);
}
/** /**
* "A PDF file can refer to the contents of another file by using a File * "A PDF file can refer to the contents of another file by using a File
* Specification (PDF 1.1)", see the spec (7.11) for more details. * Specification (PDF 1.1)", see the spec (7.11) for more details.
@ -66,26 +73,27 @@ class FileSpec {
} }
get filename() { get filename() {
if (!this._filename && this.root) { let filename = "";
const filename = pickPlatformItem(this.root) || "unnamed";
this._filename = stringToPDFString(filename) const item = pickPlatformItem(this.root);
if (item && typeof item === "string") {
filename = stringToPDFString(item)
.replaceAll("\\\\", "\\") .replaceAll("\\\\", "\\")
.replaceAll("\\/", "/") .replaceAll("\\/", "/")
.replaceAll("\\", "/"); .replaceAll("\\", "/");
} }
return this._filename; return shadow(this, "filename", filename || "unnamed");
} }
get content() { get content() {
if (!this.#contentAvailable) { if (!this.#contentAvailable) {
return null; return null;
} }
if (!this.contentRef && this.root) { this._contentRef ||= pickPlatformItem(this.root?.get("EF"));
this.contentRef = pickPlatformItem(this.root.get("EF"));
}
let content = null; let content = null;
if (this.contentRef) { if (this._contentRef) {
const fileObj = this.xref.fetchIfRef(this.contentRef); const fileObj = this.xref.fetchIfRef(this._contentRef);
if (fileObj instanceof BaseStream) { if (fileObj instanceof BaseStream) {
content = fileObj.getBytes(); content = fileObj.getBytes();
} else { } else {
@ -94,7 +102,7 @@ class FileSpec {
); );
} }
} else { } else {
warn("Embedded file specification does not have a content"); warn("Embedded file specification does not have any content");
} }
return content; return content;
} }
@ -111,7 +119,8 @@ class FileSpec {
get serializable() { get serializable() {
return { return {
filename: this.filename, rawFilename: this.filename,
filename: stripPath(this.filename),
content: this.content, content: this.content,
description: this.description, description: this.description,
}; };

View file

@ -37,7 +37,6 @@ import {
} from "../shared/util.js"; } from "../shared/util.js";
import { import {
DOMSVGFactory, DOMSVGFactory,
getFilenameFromUrl,
PDFDateString, PDFDateString,
setLayerDimensions, setLayerDimensions,
} from "./display_utils.js"; } from "./display_utils.js";
@ -2859,15 +2858,13 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
super(parameters, { isRenderable: true }); super(parameters, { isRenderable: true });
const { filename, content, description } = this.data.file; const { file } = this.data;
this.filename = getFilenameFromUrl(filename, /* onlyStripPath = */ true); this.filename = file.filename;
this.content = content; this.content = file.content;
this.linkService.eventBus?.dispatch("fileattachmentannotation", { this.linkService.eventBus?.dispatch("fileattachmentannotation", {
source: this, source: this,
filename, ...file,
content,
description,
}); });
} }

View file

@ -4033,9 +4033,12 @@ describe("annotation", function () {
idFactoryMock idFactoryMock
); );
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT); expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
expect(data.file.filename).toEqual("Test.txt"); expect(data.file).toEqual({
expect(data.file.content).toEqual(stringToBytes("Test attachment")); rawFilename: "Test.txt",
expect(data.file.description).toEqual("abc"); filename: "Test.txt",
content: stringToBytes("Test attachment"),
description: "abc",
});
}); });
}); });

View file

@ -1475,12 +1475,12 @@ describe("api", function () {
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const attachments = await pdfDoc.getAttachments(); const attachments = await pdfDoc.getAttachments();
const { filename, content, description } = attachments["foo.txt"]; expect(attachments["foo.txt"]).toEqual({
expect(filename).toEqual("foo.txt"); rawFilename: "foo.txt",
expect(content).toEqual( filename: "foo.txt",
new Uint8Array([98, 97, 114, 32, 98, 97, 122, 32, 10]) content: new Uint8Array([98, 97, 114, 32, 98, 97, 122, 32, 10]),
); description: "",
expect(description).toEqual(""); });
await loadingTask.destroy(); await loadingTask.destroy();
}); });
@ -1490,7 +1490,9 @@ describe("api", function () {
const pdfDoc = await loadingTask.promise; const pdfDoc = await loadingTask.promise;
const attachments = await pdfDoc.getAttachments(); const attachments = await pdfDoc.getAttachments();
const { filename, content, description } = attachments["empty.pdf"]; const { rawFilename, filename, content, description } =
attachments["empty.pdf"];
expect(rawFilename).toEqual("Empty page.pdf");
expect(filename).toEqual("Empty page.pdf"); expect(filename).toEqual("Empty page.pdf");
expect(content instanceof Uint8Array).toEqual(true); expect(content instanceof Uint8Array).toEqual(true);
expect(content.length).toEqual(2357); expect(content.length).toEqual(2357);

View file

@ -18,7 +18,6 @@
/** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */ /** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */
import { BaseTreeViewer } from "./base_tree_viewer.js"; import { BaseTreeViewer } from "./base_tree_viewer.js";
import { getFilenameFromUrl } from "pdfjs-lib";
import { waitOnEventOrTimeout } from "./event_utils.js"; import { waitOnEventOrTimeout } from "./event_utils.js";
/** /**
@ -122,19 +121,13 @@ class PDFAttachmentViewer extends BaseTreeViewer {
let attachmentsCount = 0; let attachmentsCount = 0;
for (const name in attachments) { for (const name in attachments) {
const item = attachments[name]; const item = attachments[name];
const content = item.content,
description = item.description,
filename = getFilenameFromUrl(
item.filename,
/* onlyStripPath = */ true
);
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "treeItem"; div.className = "treeItem";
const element = document.createElement("a"); const element = document.createElement("a");
this._bindLink(element, { content, description, filename }); this._bindLink(element, item);
element.textContent = this._normalizeTextContent(filename); element.textContent = this._normalizeTextContent(item.filename);
div.append(element); div.append(element);
@ -148,7 +141,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
/** /**
* Used to append FileAttachment annotations to the sidebar. * Used to append FileAttachment annotations to the sidebar.
*/ */
#appendAttachment({ filename, content, description }) { #appendAttachment(item) {
const renderedPromise = this._renderedCapability.promise; const renderedPromise = this._renderedCapability.promise;
renderedPromise.then(() => { renderedPromise.then(() => {
@ -158,15 +151,12 @@ class PDFAttachmentViewer extends BaseTreeViewer {
const attachments = this._attachments || Object.create(null); const attachments = this._attachments || Object.create(null);
for (const name in attachments) { for (const name in attachments) {
if (filename === name) { if (item.filename === name) {
return; // Ignore the new attachment if it already exists. return; // Ignore the new attachment if it already exists.
} }
} }
attachments[filename] = { attachments[item.filename] = item;
filename,
content,
description,
};
this.render({ this.render({
attachments, attachments,
keepRenderedCapability: true, keepRenderedCapability: true,