diff --git a/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-508-file-embed.html b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-508-file-embed.html new file mode 100644 index 000000000..5acad59d2 --- /dev/null +++ b/openhtmltopdf-examples/src/main/resources/visualtest/html/issue-508-file-embed.html @@ -0,0 +1,12 @@ + + + + + +Embedded text
document
. + + diff --git a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java index 628e7a72b..ac7b652e9 100644 --- a/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java +++ b/openhtmltopdf-examples/src/test/java/com/openhtmltopdf/nonvisualregressiontests/NonVisualRegressionTest.java @@ -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; @@ -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; @@ -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. diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/AnnotationContainer.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/AnnotationContainer.java new file mode 100644 index 000000000..d0ba6392e --- /dev/null +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/AnnotationContainer.java @@ -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); + } + } +} diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java index 0ad3c4be9..6f3944b5f 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxAccessibilityHelper.java @@ -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); diff --git a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java index bde829b29..0bacbc159 100644 --- a/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java +++ b/openhtmltopdf-pdfbox/src/main/java/com/openhtmltopdf/pdfboxout/PdfBoxFastLinkManager.java @@ -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; @@ -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.*; @@ -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 { @@ -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())); @@ -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); @@ -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);