Skip to content

Commit

Permalink
#508 #509 Implementation of file-embed link.
Browse files Browse the repository at this point in the history
Based heavily on code by @syjer whom I'm indebted to. Thanks.

Still todo:
+ Prevent duplicate file embeds. Possibly create a map of uris to PDComplexFileSpecification and reuse if encountered again. I have to peruse the PDF spec to see if this is allowed.
+ Logging on fail.
  • Loading branch information
danfickle committed Jan 21, 2021
1 parent d20825b commit 1c0d240
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<html>
<head>
<style>
@page {
size: 200px 200px;
}
</style>
</head>
<body>
Embedded <a href="stylesheets/basic.css" download="basic.css" data-content-type="text/plain">text <br/> document</a>.
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
Expand All @@ -39,6 +40,7 @@
import org.junit.Ignore;
import org.junit.Test;

import com.openhtmltopdf.outputdevice.helper.ExternalResourceControlPriority;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
import com.openhtmltopdf.testcases.TestcaseRunner;
import com.openhtmltopdf.util.Diagnostic;
Expand Down Expand Up @@ -1060,6 +1062,28 @@ public void testPr489DiagnosticConsumer() throws IOException {
.allMatch(diag -> !diag.getFormattedMessage().isEmpty()));
}

@Test
public void testIssue508FileEmbed() throws IOException {
try (PDDocument doc = run("issue-508-file-embed",
builder -> {
// File embeds are blocked by default, allow everything.
builder.useExternalResourceAccessControl((uri, type) -> true, ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI);
builder.useExternalResourceAccessControl((uri, type) -> true, ExternalResourceControlPriority.RUN_BEFORE_RESOLVING_URI);
})) {
// TODO: Renable this assertion when we have figured out a way
// to avoid duplicate file embeds when the link is broken
// up into boxes (eg. multiple lines).
// assertThat(doc.getPage(0).getAnnotations().size(), equalTo(1));

PDAnnotationFileAttachment fileAttach = (PDAnnotationFileAttachment) doc.getPage(0).getAnnotations().get(0);
assertThat(fileAttach.getFile().getFile(), equalTo("basic.css"));

// TODO:
// More asserts.

remove("issue-508-file-embed", doc);
}
}

// TODO:
// + More form controls.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.openhtmltopdf.pdfboxout;

import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;

public abstract class AnnotationContainer {
public void setRectangle(PDRectangle rectangle) {
getPdAnnotation().setRectangle(rectangle);
}

public void setPrinted(boolean printed) {
getPdAnnotation().setPrinted(printed);
}

public void setQuadPoints(float[] quadPoints) {};

public abstract void setBorderStyle(PDBorderStyleDictionary styleDict);

public abstract PDAnnotation getPdAnnotation();

public static class PDAnnotationFileAttachmentContainer extends AnnotationContainer {
private final PDAnnotationFileAttachment pdAnnotationFileAttachment;

public PDAnnotationFileAttachmentContainer(PDAnnotationFileAttachment pdAnnotationFileAttachment) {
this.pdAnnotationFileAttachment = pdAnnotationFileAttachment;
}

@Override
public PDAnnotation getPdAnnotation() {
return pdAnnotationFileAttachment;
}

@Override
public void setBorderStyle(PDBorderStyleDictionary styleDict) {
pdAnnotationFileAttachment.setBorderStyle(styleDict);
}
}

public static class PDAnnotationLinkContainer extends AnnotationContainer {
private final PDAnnotationLink pdAnnotationLink;

public PDAnnotationLinkContainer(PDAnnotationLink pdAnnotationLink) {
this.pdAnnotationLink = pdAnnotationLink;
}

@Override
public PDAnnotation getPdAnnotation() {
return pdAnnotationLink;
}

@Override
public void setQuadPoints(float[] quadPoints) {
pdAnnotationLink.setQuadPoints(quadPoints);
}

@Override
public void setBorderStyle(PDBorderStyleDictionary styleDict) {
pdAnnotationLink.setBorderStyle(styleDict);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1210,17 +1210,17 @@ private static class AnnotationWithStructureParent {
PDAnnotation annotation;
}

public void addLink(Box anchor, Box target, PDAnnotationLink annotation, PDPage page) {
public void addLink(Box anchor, Box target, PDAnnotation pdAnnotation, PDPage page) {
PDStructureElement struct = getStructualElementForBox(anchor);
if (struct != null) {
// We have to append the link annotationobject reference as a kid of its associated structure element.
PDObjectReference ref = new PDObjectReference();
ref.setReferencedObject(annotation);
ref.setReferencedObject(pdAnnotation);
struct.appendKid(ref);

// We also need to save the pair so we can add it to the number tree for reverse lookup.
AnnotationWithStructureParent annotStructParentPair = new AnnotationWithStructureParent();
annotStructParentPair.annotation = annotation;
annotStructParentPair.annotation = pdAnnotation;
annotStructParentPair.structureParent = struct;

_pageItems._pageAnnotations.add(annotStructParentPair);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.openhtmltopdf.extend.NamespaceHandler;
import com.openhtmltopdf.extend.ReplacedElement;
import com.openhtmltopdf.layout.SharedContext;
import com.openhtmltopdf.outputdevice.helper.ExternalResourceType;
import com.openhtmltopdf.pdfboxout.PdfBoxLinkManager.IPdfBoxElementWithShapedLinks;
import com.openhtmltopdf.pdfboxout.quads.KongAlgo;
import com.openhtmltopdf.pdfboxout.quads.Triangle;
Expand All @@ -14,19 +15,26 @@
import com.openhtmltopdf.util.XRLog;

import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.w3c.dom.Element;

import java.awt.*;
import java.awt.geom.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.*;
Expand Down Expand Up @@ -225,29 +233,91 @@ private void addUriAsLink(RenderingContext c, Box box, PDPage page, float pageHe

PDAnnotationLink annot = new PDAnnotationLink();
annot.setAction(action);
if (!placeAnnotation(transform, linkShape, targetArea, annot))

AnnotationContainer annotContainer = new AnnotationContainer.PDAnnotationLinkContainer(annot);

if (!placeAnnotation(transform, linkShape, targetArea, annotContainer))
return;

addLinkToPage(page, annot, box, target);
addLinkToPage(page, annotContainer, box, target);
} else {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.GENERAL_PDF_COULD_NOT_FIND_VALID_TARGET_FOR_LINK, uri);
}
} else if (isURI(uri)) {
PDActionURI uriAct = new PDActionURI();
uriAct.setURI(uri);
AnnotationContainer annotContainer = null;

Rectangle2D targetArea = checkLinkArea(page, c, box, pageHeight, transform, linkShape);
if (targetArea == null) {
return;
}
PDAnnotationLink annot = new PDAnnotationLink();
annot.setAction(uriAct);
if (!placeAnnotation(transform, linkShape, targetArea, annot))
return;
if (!elem.hasAttribute("download")) {
PDActionURI uriAct = new PDActionURI();
uriAct.setURI(uri);

addLinkToPage(page, annot, box, null);
}
}
PDAnnotationLink annot = new PDAnnotationLink();
annot.setAction(uriAct);

annotContainer = new AnnotationContainer.PDAnnotationLinkContainer(annot);
} else {
annotContainer = createFileEmbedLinkAnnotation(elem, uri);
}

if (annotContainer != null) {
Rectangle2D targetArea = checkLinkArea(page, c, box, pageHeight, transform, linkShape);

if (targetArea == null) {
return;
}

if (!placeAnnotation(transform, linkShape, targetArea, annotContainer)) {
return;
}

addLinkToPage(page, annotContainer, box, null);
}
}
}

private AnnotationContainer createFileEmbedLinkAnnotation(
Element elem, String uri) {
byte[] file = _sharedContext.getUserAgentCallback().getBinaryResource(uri, ExternalResourceType.FILE_EMBED);

if (file != null) {
try {
PDComplexFileSpecification fs = new PDComplexFileSpecification();
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(_od.getWriter(), new ByteArrayInputStream(file));

String contentType = elem.getAttribute("data-content-type").isEmpty() ?
"application/octet-stream" :
elem.getAttribute("data-content-type");

embeddedFile.setSubtype(contentType);

fs.setEmbeddedFile(embeddedFile);

String fileName = elem.getAttribute("download");

fs.setFile(fileName);
fs.setFileUnicode(fileName);

PDAnnotationFileAttachment annotationFileAttachment = new PDAnnotationFileAttachment();
annotationFileAttachment.setFile(fs);

// hide the pin icon used by various pdf reader for signaling an embedded file
PDAppearanceDictionary appearanceDictionary = new PDAppearanceDictionary();
PDAppearanceStream appearanceStream = new PDAppearanceStream(_od.getWriter());
appearanceStream.setResources(new PDResources());
appearanceDictionary.setNormalAppearance(appearanceStream);
annotationFileAttachment.setAppearance(appearanceDictionary);

return new AnnotationContainer.PDAnnotationFileAttachmentContainer(annotationFileAttachment);
} catch (IOException e) {
// TODO
//XRLog.exception("Was not able to create an embedded file for embedding with uri " + uri, e);
}
} else {
// TODO
//XRLog.general("Was not able to load file from uri for embedding" + uri);
}

return null;
}

private static boolean isURI(String uri) {
try {
Expand All @@ -260,7 +330,7 @@ private static boolean isURI(String uri) {

@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean placeAnnotation(AffineTransform transform, Shape linkShape, Rectangle2D targetArea,
PDAnnotationLink annot) {
AnnotationContainer annot) {
annot.setRectangle(new PDRectangle((float) targetArea.getMinX(), (float) targetArea.getMinY(),
(float) targetArea.getWidth(), (float) targetArea.getHeight()));

Expand Down Expand Up @@ -377,7 +447,8 @@ static QuadPointShape mapShapeToQuadPoints(AffineTransform transform, Shape link
return result;
}

private void addLinkToPage(PDPage page, PDAnnotationLink annot, Box anchor, Box target) {
private void addLinkToPage(
PDPage page, AnnotationContainer annot, Box anchor, Box target) {
PDBorderStyleDictionary styleDict = new PDBorderStyleDictionary();
styleDict.setWidth(0);
styleDict.setStyle(PDBorderStyleDictionary.STYLE_SOLID);
Expand All @@ -391,10 +462,10 @@ private void addLinkToPage(PDPage page, PDAnnotationLink annot, Box anchor, Box
page.setAnnotations(annots);
}

annots.add(annot);
annots.add(annot.getPdAnnotation());

if (_pdfUa != null) {
_pdfUa.addLink(anchor, target, annot, page);
_pdfUa.addLink(anchor, target, annot.getPdAnnotation(), page);
}
} catch (IOException e) {
throw new PdfContentStreamAdapter.PdfException("processLink", e);
Expand Down

0 comments on commit 1c0d240

Please sign in to comment.