From bf4e36d1b5fb9a3728c6441f68d9435f235c8e4b Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 30 Apr 2024 18:51:31 +0200 Subject: [PATCH] [api-minor] Expose the /Desc-attribute of file attachments in the viewer (issue 18030) In the viewer this will be displayed in the `title` of the hyperlink, which is probably the best we can do here given how the viewer is implemented. --- src/core/file_spec.js | 16 ++++++++++++---- src/display/annotation_layer.js | 6 +++++- test/pdfs/.gitignore | 1 + test/pdfs/issue18030.pdf | Bin 0 -> 12788 bytes test/unit/annotation_spec.js | 3 ++- test/unit/api_spec.js | 23 ++++++++++++++++++++--- web/pdf_attachment_viewer.js | 11 ++++++++--- 7 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 test/pdfs/issue18030.pdf diff --git a/src/core/file_spec.js b/src/core/file_spec.js index 8ce91c35275e6..11ddb38e452b6 100644 --- a/src/core/file_spec.js +++ b/src/core/file_spec.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { stringToPDFString, warn } from "../shared/util.js"; +import { shadow, stringToPDFString, warn } from "../shared/util.js"; import { BaseStream } from "./base_stream.js"; import { Dict } from "./primitives.js"; @@ -53,9 +53,6 @@ class FileSpec { if (root.has("FS")) { this.fs = root.get("FS"); } - this.description = root.has("Desc") - ? stringToPDFString(root.get("Desc")) - : ""; if (root.has("RF")) { warn("Related file specifications are not supported"); } @@ -102,10 +99,21 @@ class FileSpec { return content; } + get description() { + let description = ""; + + const desc = this.root?.get("Desc"); + if (desc && typeof desc === "string") { + description = stringToPDFString(desc); + } + return shadow(this, "description", description); + } + get serializable() { return { filename: this.filename, content: this.content, + description: this.description, }; } } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index e6555745c15bd..6be8d71e9da90 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -857,6 +857,9 @@ class LinkAnnotationElement extends AnnotationElement { */ #bindAttachment(link, attachment, dest = null) { link.href = this.linkService.getAnchorUrl(""); + if (attachment.description) { + link.title = attachment.description; + } link.onclick = () => { this.downloadManager?.openOrDownloadData( attachment.content, @@ -2856,7 +2859,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement { constructor(parameters) { super(parameters, { isRenderable: true }); - const { filename, content } = this.data.file; + const { filename, content, description } = this.data.file; this.filename = getFilenameFromUrl(filename, /* onlyStripPath = */ true); this.content = content; @@ -2864,6 +2867,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement { source: this, filename, content, + description, }); } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 0f516162583a0..1f1198aa80d8c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -54,6 +54,7 @@ !issue17056.pdf !issue17679.pdf !issue17679_2.pdf +!issue18030.pdf !issue14953.pdf !issue15367.pdf !issue15372.pdf diff --git a/test/pdfs/issue18030.pdf b/test/pdfs/issue18030.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a30b7ff2ca1c155b31987e16f34cd430956b44c9 GIT binary patch literal 12788 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 literal 0 HcmV?d00001 diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 5432a4ab63511..f118eae3d5e5e 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4005,7 +4005,7 @@ describe("annotation", function () { const fileSpecRef = Ref.get(19, 0); const fileSpecDict = new Dict(); fileSpecDict.set("Type", Name.get("Filespec")); - fileSpecDict.set("Desc", ""); + fileSpecDict.set("Desc", "abc"); fileSpecDict.set("EF", embeddedFileDict); fileSpecDict.set("UF", "Test.txt"); @@ -4035,6 +4035,7 @@ describe("annotation", function () { expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT); expect(data.file.filename).toEqual("Test.txt"); expect(data.file.content).toEqual(stringToBytes("Test attachment")); + expect(data.file.description).toEqual("abc"); }); }); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 08db9bfb80bde..6a0c034a204b3 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1475,11 +1475,28 @@ describe("api", function () { const pdfDoc = await loadingTask.promise; const attachments = await pdfDoc.getAttachments(); - const attachment = attachments["foo.txt"]; - expect(attachment.filename).toEqual("foo.txt"); - expect(attachment.content).toEqual( + const { filename, content, description } = attachments["foo.txt"]; + expect(filename).toEqual("foo.txt"); + expect(content).toEqual( new Uint8Array([98, 97, 114, 32, 98, 97, 122, 32, 10]) ); + expect(description).toEqual(""); + + await loadingTask.destroy(); + }); + + it("gets attachments, with /Desc", async function () { + const loadingTask = getDocument(buildGetDocumentParams("issue18030.pdf")); + const pdfDoc = await loadingTask.promise; + const attachments = await pdfDoc.getAttachments(); + + const { filename, content, description } = attachments["empty.pdf"]; + expect(filename).toEqual("Empty page.pdf"); + expect(content instanceof Uint8Array).toEqual(true); + expect(content.length).toEqual(2357); + expect(description).toEqual( + "SHA512: 06bec56808f93846f1d41ff0be4e54079c1291b860378c801c0f35f1d127a8680923ff6de59bd5a9692f01f0d97ca4f26da178ed03635fa4813d86c58a6c981a" + ); await loadingTask.destroy(); }); diff --git a/web/pdf_attachment_viewer.js b/web/pdf_attachment_viewer.js index 53aef16db1d64..eb0cfd9e612f4 100644 --- a/web/pdf_attachment_viewer.js +++ b/web/pdf_attachment_viewer.js @@ -94,7 +94,10 @@ class PDFAttachmentViewer extends BaseTreeViewer { /** * @protected */ - _bindLink(element, { content, filename }) { + _bindLink(element, { content, description, filename }) { + if (description) { + element.title = description; + } element.onclick = () => { this.downloadManager.openOrDownloadData(content, filename); return false; @@ -120,6 +123,7 @@ class PDFAttachmentViewer extends BaseTreeViewer { for (const name in attachments) { const item = attachments[name]; const content = item.content, + description = item.description, filename = getFilenameFromUrl( item.filename, /* onlyStripPath = */ true @@ -129,7 +133,7 @@ class PDFAttachmentViewer extends BaseTreeViewer { div.className = "treeItem"; const element = document.createElement("a"); - this._bindLink(element, { content, filename }); + this._bindLink(element, { content, description, filename }); element.textContent = this._normalizeTextContent(filename); div.append(element); @@ -144,7 +148,7 @@ class PDFAttachmentViewer extends BaseTreeViewer { /** * Used to append FileAttachment annotations to the sidebar. */ - #appendAttachment({ filename, content }) { + #appendAttachment({ filename, content, description }) { const renderedPromise = this._renderedCapability.promise; renderedPromise.then(() => { @@ -161,6 +165,7 @@ class PDFAttachmentViewer extends BaseTreeViewer { attachments[filename] = { filename, content, + description, }; this.render({ attachments,