diff --git a/src/core/annotation.js b/src/core/annotation.js index 6aa8b4d20..aa312e157 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -80,15 +80,19 @@ class AnnotationFactory { // Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used // with certain Annotations, from throwing and thus breaking parsing: pdfManager.ensureCatalog("baseUrl"), + // Only necessary in the `Catalog.parseDestDictionary`-method, + // when parsing "GoToE" actions: + pdfManager.ensureCatalog("attachments"), pdfManager.ensureDoc("xfaDatasets"), collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1, - ]).then(([acroForm, baseUrl, xfaDatasets, pageIndex]) => + ]).then(([acroForm, baseUrl, attachments, xfaDatasets, pageIndex]) => pdfManager.ensure(this, "_create", [ xref, ref, pdfManager, idFactory, acroForm, + attachments, xfaDatasets, collectFields, pageIndex, @@ -105,6 +109,7 @@ class AnnotationFactory { pdfManager, idFactory, acroForm, + attachments = null, xfaDatasets, collectFields, pageIndex = -1 @@ -130,6 +135,7 @@ class AnnotationFactory { id, pdfManager, acroForm: acroForm instanceof Dict ? acroForm : Dict.empty, + attachments, xfaDatasets, collectFields, pageIndex, @@ -2893,6 +2899,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { destDict: params.dict, resultObj: this.data, docBaseUrl: params.pdfManager.docBaseUrl, + docAttachments: params.attachments, }); } @@ -3221,6 +3228,7 @@ class LinkAnnotation extends Annotation { destDict: params.dict, resultObj: this.data, docBaseUrl: params.pdfManager.docBaseUrl, + docAttachments: params.attachments, }); } } diff --git a/src/core/catalog.js b/src/core/catalog.js index f60d740f3..561d6a9be 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -307,6 +307,7 @@ class Catalog { destDict: outlineDict, resultObj: data, docBaseUrl: this.pdfManager.docBaseUrl, + docAttachments: this.attachments, }); const title = outlineDict.get("Title"); const flags = outlineDict.get("F") || 0; @@ -325,6 +326,7 @@ class Catalog { const outlineItem = { action: data.action, + attachment: data.attachment, dest: data.dest, url: data.url, unsafeUrl: data.unsafeUrl, @@ -1412,6 +1414,8 @@ class Catalog { * properties will be placed. * @property {string} [docBaseUrl] - The document base URL that is used when * attempting to recover valid absolute URLs from relative ones. + * @property {Object} [docAttachments] - The document attachments (may not + * exist in most PDF documents). */ /** @@ -1430,6 +1434,7 @@ class Catalog { return; } const docBaseUrl = params.docBaseUrl || null; + const docAttachments = params.docAttachments || null; let action = destDict.get("A"), url, @@ -1526,6 +1531,26 @@ class Catalog { } break; + case "GoToE": + const target = action.get("T"); + let attachment; + + if (docAttachments && target instanceof Dict) { + const relationship = target.get("R"); + const name = target.get("N"); + + if (isName(relationship, "C") && typeof name === "string") { + attachment = docAttachments[stringToPDFString(name)]; + } + } + + if (attachment) { + resultObj.attachment = attachment; + } else { + warn(`parseDestDictionary - unimplemented "GoToE" action.`); + } + break; + case "Named": const namedAction = action.get("N"); if (namedAction instanceof Name) { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 9839506e8..85cb93c9c 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -595,6 +595,9 @@ class LinkAnnotationElement extends AnnotationElement { } else if (data.action) { this._bindNamedAction(link, data.action); isBound = true; + } else if (data.attachment) { + this._bindAttachment(link, data.attachment); + isBound = true; } else if (data.setOCGState) { this.#bindSetOCGState(link, data.setOCGState); isBound = true; @@ -679,6 +682,24 @@ class LinkAnnotationElement extends AnnotationElement { link.className = "internalLink"; } + /** + * Bind attachments to the link element. + * @param {Object} link + * @param {Object} attachment + */ + _bindAttachment(link, attachment) { + link.href = this.linkService.getAnchorUrl(""); + link.onclick = () => { + this.downloadManager?.openOrDownloadData( + this.container, + attachment.content, + attachment.filename + ); + return false; + }; + link.className = "internalLink"; + } + /** * Bind SetOCGState actions to the link element. * @param {Object} link diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 1b0d41af4..80524c365 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -49,6 +49,7 @@ !issue7426.pdf !issue7439.pdf !issue7847_radial.pdf +!issue8844.pdf !issue14953.pdf !issue15367.pdf !issue15372.pdf diff --git a/test/pdfs/issue8844.pdf b/test/pdfs/issue8844.pdf new file mode 100644 index 000000000..c1f25ffe4 Binary files /dev/null and b/test/pdfs/issue8844.pdf differ diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 591f94624..cea08e235 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -15,6 +15,7 @@ import { AnnotationMode, + AnnotationType, createPromiseCapability, FontType, ImageKind, @@ -1536,6 +1537,7 @@ describe("api", function () { expect(outline[4]).toEqual({ action: null, + attachment: undefined, dest: "Händel -- Halle🎆lujah", url: null, unsafeUrl: undefined, @@ -1562,6 +1564,7 @@ describe("api", function () { expect(outline[1]).toEqual({ action: "PrevPage", + attachment: undefined, dest: null, url: null, unsafeUrl: undefined, @@ -1588,6 +1591,7 @@ describe("api", function () { expect(outline[0]).toEqual({ action: null, + attachment: undefined, dest: null, url: null, unsafeUrl: undefined, @@ -2155,6 +2159,23 @@ describe("api", function () { ]); }); + it("gets annotations containing GoToE action (issue 8844)", async function () { + const loadingTask = getDocument(buildGetDocumentParams("issue8844.pdf")); + const pdfDoc = await loadingTask.promise; + const pdfPage = await pdfDoc.getPage(1); + const annotations = await pdfPage.getAnnotations(); + + expect(annotations.length).toEqual(1); + expect(annotations[0].annotationType).toEqual(AnnotationType.LINK); + + const { filename, content } = annotations[0].attachment; + expect(filename).toEqual("man.pdf"); + expect(content instanceof Uint8Array).toEqual(true); + expect(content.length).toEqual(4508); + + await loadingTask.destroy(); + }); + it("gets text content", async function () { const defaultPromise = page.getTextContent(); const parametersPromise = page.getTextContent({ diff --git a/web/app.js b/web/app.js index 37a655b39..046f498ee 100644 --- a/web/app.js +++ b/web/app.js @@ -636,6 +636,7 @@ const PDFViewerApplication = { container: appConfig.sidebar.outlineView, eventBus, linkService: pdfLinkService, + downloadManager, }); this.pdfAttachmentViewer = new PDFAttachmentViewer({ diff --git a/web/pdf_outline_viewer.js b/web/pdf_outline_viewer.js index 9bcb8179a..42ac42c6c 100644 --- a/web/pdf_outline_viewer.js +++ b/web/pdf_outline_viewer.js @@ -20,8 +20,9 @@ import { SidebarView } from "./ui_utils.js"; /** * @typedef {Object} PDFOutlineViewerOptions * @property {HTMLDivElement} container - The viewer element. - * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {EventBus} eventBus - The application event bus. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {DownloadManager} downloadManager - The download manager. */ /** @@ -37,6 +38,7 @@ class PDFOutlineViewer extends BaseTreeViewer { constructor(options) { super(options); this.linkService = options.linkService; + this.downloadManager = options.downloadManager; this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this)); this.eventBus._on( @@ -109,7 +111,10 @@ class PDFOutlineViewer extends BaseTreeViewer { /** * @private */ - _bindLink(element, { url, newWindow, action, dest, setOCGState }) { + _bindLink( + element, + { url, newWindow, action, attachment, dest, setOCGState } + ) { const { linkService } = this; if (url) { @@ -124,6 +129,18 @@ class PDFOutlineViewer extends BaseTreeViewer { }; return; } + if (attachment) { + element.href = linkService.getAnchorUrl(""); + element.onclick = () => { + this.downloadManager.openOrDownloadData( + element, + attachment.content, + attachment.filename + ); + return false; + }; + return; + } if (setOCGState) { element.href = linkService.getAnchorUrl(""); element.onclick = () => {