[api-minor] Add partial support for the "GoToE" action (issue 8844)

*Please note:* The referenced issue is the only mention that I can find, in either GitHub or Bugzilla, of "GoToE" actions.
Hence why I've purposely settled for a very simple, and partial, "GoToE" implementation to avoid complicating things initially.[1] In particular, this patch only supports "GoToE" actions that references the /EmbeddedFiles-dict in the PDF document.

See https://web.archive.org/web/20220309040754if_/https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf#G11.2048909

---
[1] Usually I always prefer having *real-world* test-cases to work with, whenever I'm implementing new features.
This commit is contained in:
Jonas Jenwald 2022-10-03 17:55:13 +02:00
parent 8c59cc72a3
commit ce66fefbff
8 changed files with 97 additions and 3 deletions

View File

@ -80,15 +80,19 @@ class AnnotationFactory {
// Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used // Only necessary to prevent the `pdfManager.docBaseUrl`-getter, used
// with certain Annotations, from throwing and thus breaking parsing: // with certain Annotations, from throwing and thus breaking parsing:
pdfManager.ensureCatalog("baseUrl"), pdfManager.ensureCatalog("baseUrl"),
// Only necessary in the `Catalog.parseDestDictionary`-method,
// when parsing "GoToE" actions:
pdfManager.ensureCatalog("attachments"),
pdfManager.ensureDoc("xfaDatasets"), pdfManager.ensureDoc("xfaDatasets"),
collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1, collectFields ? this._getPageIndex(xref, ref, pdfManager) : -1,
]).then(([acroForm, baseUrl, xfaDatasets, pageIndex]) => ]).then(([acroForm, baseUrl, attachments, xfaDatasets, pageIndex]) =>
pdfManager.ensure(this, "_create", [ pdfManager.ensure(this, "_create", [
xref, xref,
ref, ref,
pdfManager, pdfManager,
idFactory, idFactory,
acroForm, acroForm,
attachments,
xfaDatasets, xfaDatasets,
collectFields, collectFields,
pageIndex, pageIndex,
@ -105,6 +109,7 @@ class AnnotationFactory {
pdfManager, pdfManager,
idFactory, idFactory,
acroForm, acroForm,
attachments = null,
xfaDatasets, xfaDatasets,
collectFields, collectFields,
pageIndex = -1 pageIndex = -1
@ -130,6 +135,7 @@ class AnnotationFactory {
id, id,
pdfManager, pdfManager,
acroForm: acroForm instanceof Dict ? acroForm : Dict.empty, acroForm: acroForm instanceof Dict ? acroForm : Dict.empty,
attachments,
xfaDatasets, xfaDatasets,
collectFields, collectFields,
pageIndex, pageIndex,
@ -2893,6 +2899,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
destDict: params.dict, destDict: params.dict,
resultObj: this.data, resultObj: this.data,
docBaseUrl: params.pdfManager.docBaseUrl, docBaseUrl: params.pdfManager.docBaseUrl,
docAttachments: params.attachments,
}); });
} }
@ -3221,6 +3228,7 @@ class LinkAnnotation extends Annotation {
destDict: params.dict, destDict: params.dict,
resultObj: this.data, resultObj: this.data,
docBaseUrl: params.pdfManager.docBaseUrl, docBaseUrl: params.pdfManager.docBaseUrl,
docAttachments: params.attachments,
}); });
} }
} }

View File

@ -307,6 +307,7 @@ class Catalog {
destDict: outlineDict, destDict: outlineDict,
resultObj: data, resultObj: data,
docBaseUrl: this.pdfManager.docBaseUrl, docBaseUrl: this.pdfManager.docBaseUrl,
docAttachments: this.attachments,
}); });
const title = outlineDict.get("Title"); const title = outlineDict.get("Title");
const flags = outlineDict.get("F") || 0; const flags = outlineDict.get("F") || 0;
@ -325,6 +326,7 @@ class Catalog {
const outlineItem = { const outlineItem = {
action: data.action, action: data.action,
attachment: data.attachment,
dest: data.dest, dest: data.dest,
url: data.url, url: data.url,
unsafeUrl: data.unsafeUrl, unsafeUrl: data.unsafeUrl,
@ -1412,6 +1414,8 @@ class Catalog {
* properties will be placed. * properties will be placed.
* @property {string} [docBaseUrl] - The document base URL that is used when * @property {string} [docBaseUrl] - The document base URL that is used when
* attempting to recover valid absolute URLs from relative ones. * 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; return;
} }
const docBaseUrl = params.docBaseUrl || null; const docBaseUrl = params.docBaseUrl || null;
const docAttachments = params.docAttachments || null;
let action = destDict.get("A"), let action = destDict.get("A"),
url, url,
@ -1526,6 +1531,26 @@ class Catalog {
} }
break; 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": case "Named":
const namedAction = action.get("N"); const namedAction = action.get("N");
if (namedAction instanceof Name) { if (namedAction instanceof Name) {

View File

@ -595,6 +595,9 @@ class LinkAnnotationElement extends AnnotationElement {
} else if (data.action) { } else if (data.action) {
this._bindNamedAction(link, data.action); this._bindNamedAction(link, data.action);
isBound = true; isBound = true;
} else if (data.attachment) {
this._bindAttachment(link, data.attachment);
isBound = true;
} else if (data.setOCGState) { } else if (data.setOCGState) {
this.#bindSetOCGState(link, data.setOCGState); this.#bindSetOCGState(link, data.setOCGState);
isBound = true; isBound = true;
@ -679,6 +682,24 @@ class LinkAnnotationElement extends AnnotationElement {
link.className = "internalLink"; 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. * Bind SetOCGState actions to the link element.
* @param {Object} link * @param {Object} link

View File

@ -49,6 +49,7 @@
!issue7426.pdf !issue7426.pdf
!issue7439.pdf !issue7439.pdf
!issue7847_radial.pdf !issue7847_radial.pdf
!issue8844.pdf
!issue14953.pdf !issue14953.pdf
!issue15367.pdf !issue15367.pdf
!issue15372.pdf !issue15372.pdf

BIN
test/pdfs/issue8844.pdf Normal file

Binary file not shown.

View File

@ -15,6 +15,7 @@
import { import {
AnnotationMode, AnnotationMode,
AnnotationType,
createPromiseCapability, createPromiseCapability,
FontType, FontType,
ImageKind, ImageKind,
@ -1536,6 +1537,7 @@ describe("api", function () {
expect(outline[4]).toEqual({ expect(outline[4]).toEqual({
action: null, action: null,
attachment: undefined,
dest: "Händel -- Halle🎆lujah", dest: "Händel -- Halle🎆lujah",
url: null, url: null,
unsafeUrl: undefined, unsafeUrl: undefined,
@ -1562,6 +1564,7 @@ describe("api", function () {
expect(outline[1]).toEqual({ expect(outline[1]).toEqual({
action: "PrevPage", action: "PrevPage",
attachment: undefined,
dest: null, dest: null,
url: null, url: null,
unsafeUrl: undefined, unsafeUrl: undefined,
@ -1588,6 +1591,7 @@ describe("api", function () {
expect(outline[0]).toEqual({ expect(outline[0]).toEqual({
action: null, action: null,
attachment: undefined,
dest: null, dest: null,
url: null, url: null,
unsafeUrl: undefined, 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 () { it("gets text content", async function () {
const defaultPromise = page.getTextContent(); const defaultPromise = page.getTextContent();
const parametersPromise = page.getTextContent({ const parametersPromise = page.getTextContent({

View File

@ -636,6 +636,7 @@ const PDFViewerApplication = {
container: appConfig.sidebar.outlineView, container: appConfig.sidebar.outlineView,
eventBus, eventBus,
linkService: pdfLinkService, linkService: pdfLinkService,
downloadManager,
}); });
this.pdfAttachmentViewer = new PDFAttachmentViewer({ this.pdfAttachmentViewer = new PDFAttachmentViewer({

View File

@ -20,8 +20,9 @@ import { SidebarView } from "./ui_utils.js";
/** /**
* @typedef {Object} PDFOutlineViewerOptions * @typedef {Object} PDFOutlineViewerOptions
* @property {HTMLDivElement} container - The viewer element. * @property {HTMLDivElement} container - The viewer element.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {EventBus} eventBus - The application event bus. * @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) { constructor(options) {
super(options); super(options);
this.linkService = options.linkService; this.linkService = options.linkService;
this.downloadManager = options.downloadManager;
this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this)); this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this));
this.eventBus._on( this.eventBus._on(
@ -109,7 +111,10 @@ class PDFOutlineViewer extends BaseTreeViewer {
/** /**
* @private * @private
*/ */
_bindLink(element, { url, newWindow, action, dest, setOCGState }) { _bindLink(
element,
{ url, newWindow, action, attachment, dest, setOCGState }
) {
const { linkService } = this; const { linkService } = this;
if (url) { if (url) {
@ -124,6 +129,18 @@ class PDFOutlineViewer extends BaseTreeViewer {
}; };
return; return;
} }
if (attachment) {
element.href = linkService.getAnchorUrl("");
element.onclick = () => {
this.downloadManager.openOrDownloadData(
element,
attachment.content,
attachment.filename
);
return false;
};
return;
}
if (setOCGState) { if (setOCGState) {
element.href = linkService.getAnchorUrl(""); element.href = linkService.getAnchorUrl("");
element.onclick = () => { element.onclick = () => {