[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
// 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,
});
}
}

View File

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

View File

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

View File

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

BIN
test/pdfs/issue8844.pdf Normal file

Binary file not shown.

View File

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

View File

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

View File

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