From 8a68d2813b2a3a07c84ebfef8466d983dfee153c Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Tue, 12 Nov 2024 01:06:53 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Extracts=20method=20=F0=9F=93=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …such that an alternative for inline SVGs can be added in future commits. OX-9272 --- .../pdf/ImageReplacedElementFactory.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java b/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java index 5d3a34545..8c3156a14 100644 --- a/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java +++ b/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java @@ -20,6 +20,7 @@ import sirius.kernel.health.Exceptions; import sirius.web.templates.pdf.handlers.PdfReplaceHandler; +import javax.annotation.Nonnull; import java.util.List; import java.util.Optional; @@ -57,25 +58,36 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, return null; } + return this.tryCreateReplacedImageElement(element, userAgentCallback, cssWidth, cssHeight) + .orElseGet(() -> super.createReplacedElement(layoutContext, + box, + userAgentCallback, + cssWidth, + cssHeight)); + } + + private Optional tryCreateReplacedImageElement(@Nonnull Element element, + UserAgentCallback userAgentCallback, + int cssWidth, + int cssHeight) { String nodeName = element.getNodeName(); if (!TAG_TYPE_IMG.equals(nodeName)) { - return super.createReplacedElement(layoutContext, box, userAgentCallback, cssWidth, cssHeight); + return Optional.empty(); } String source = rewriteLegacyUrl(element.getAttribute(ATTR_SRC)); if (Strings.isEmpty(source)) { - return super.createReplacedElement(layoutContext, box, userAgentCallback, cssWidth, cssHeight); + return Optional.empty(); } try { String protocol = Strings.split(source, "://").getFirst(); PdfReplaceHandler handler = findHandler(protocol); - return new AsyncLoadedImageElement(handler, userAgentCallback, source, cssWidth, cssHeight); + return Optional.of(new AsyncLoadedImageElement(handler, userAgentCallback, source, cssWidth, cssHeight)); } catch (Exception exception) { Exceptions.handle(exception); + return Optional.empty(); } - - return super.createReplacedElement(layoutContext, box, userAgentCallback, cssWidth, cssHeight); } private PdfReplaceHandler findHandler(String protocol) { From 4e291815082ae3a414d1aac3728bf782d8c27ae0 Mon Sep 17 00:00:00 2001 From: Jakob Vogel Date: Tue, 12 Nov 2024 02:22:51 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Renders=20inline=20SVGs=20into=20PDFs=20?= =?UTF-8?q?=F0=9F=91=A8=E2=80=8D=F0=9F=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …via Apache Batik as Flying Saucer does not intrinsically support that. This solution is heavily based on https://stackoverflow.com/questions/37056791/svg-integration-in-pdf-using-flying-saucer and linked web resources. OX-9272 --- pom.xml | 7 + .../pdf/ImageReplacedElementFactory.java | 33 ++++- .../web/templates/pdf/InlinedSvgElement.java | 120 ++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/main/java/sirius/web/templates/pdf/InlinedSvgElement.java diff --git a/pom.xml b/pom.xml index afbc5f8c9..78d4553be 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,13 @@ 1.17.2 + + + org.apache.xmlgraphics + batik-all + 1.18 + + org.apache.poi diff --git a/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java b/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java index 8c3156a14..6eb2c5a7d 100644 --- a/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java +++ b/src/main/java/sirius/web/templates/pdf/ImageReplacedElementFactory.java @@ -8,6 +8,7 @@ package sirius.web.templates.pdf; +import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xhtmlrenderer.extend.ReplacedElement; import org.xhtmlrenderer.extend.UserAgentCallback; @@ -21,11 +22,15 @@ import sirius.web.templates.pdf.handlers.PdfReplaceHandler; import javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import java.util.List; import java.util.Optional; /** - * Used by the XHTMLRenderer (creating PDFs) to replace img elements by their referenced image. + * Used by the XHTMLRenderer (creating PDFs) to replace {@code img} elements by their referenced image and to render + * inline {@code svg} elements. *

* Alongside http different URI protocols are supported. These are handled by classes extending * {@link PdfReplaceHandler}. @@ -33,6 +38,8 @@ public class ImageReplacedElementFactory extends ITextReplacedElementFactory { private static final String TAG_TYPE_IMG = "img"; + private static final String TAG_TYPE_SVG = "svg"; + private static final String ATTR_SRC = "src"; @PriorityParts(PdfReplaceHandler.class) @@ -59,6 +66,7 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, } return this.tryCreateReplacedImageElement(element, userAgentCallback, cssWidth, cssHeight) + .or(() -> this.tryCreateReplacedSvgElement(element, cssWidth, cssHeight)) .orElseGet(() -> super.createReplacedElement(layoutContext, box, userAgentCallback, @@ -66,6 +74,29 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext, cssHeight)); } + private Optional tryCreateReplacedSvgElement(@Nonnull Element element, + int cssWidth, + int cssHeight) { + String nodeName = element.getNodeName(); + if (!TAG_TYPE_SVG.equals(nodeName)) { + return Optional.empty(); + } + + try { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + + Document svgDocument = documentBuilder.newDocument(); + Element svgElement = (Element) svgDocument.importNode(element, true); + svgDocument.appendChild(svgElement); + + return Optional.of(new InlinedSvgElement(svgDocument, cssWidth, cssHeight)); + } catch (ParserConfigurationException exception) { + Exceptions.handle(exception); + return Optional.empty(); + } + } + private Optional tryCreateReplacedImageElement(@Nonnull Element element, UserAgentCallback userAgentCallback, int cssWidth, diff --git a/src/main/java/sirius/web/templates/pdf/InlinedSvgElement.java b/src/main/java/sirius/web/templates/pdf/InlinedSvgElement.java new file mode 100644 index 000000000..b01c8042d --- /dev/null +++ b/src/main/java/sirius/web/templates/pdf/InlinedSvgElement.java @@ -0,0 +1,120 @@ +/* + * Made with all the love in the world + * by scireum in Remshalden, Germany + * + * Copyright by scireum GmbH + * http://www.scireum.de - info@scireum.de + */ + +package sirius.web.templates.pdf; + +import com.lowagie.text.pdf.PdfContentByte; +import com.lowagie.text.pdf.PdfTemplate; +import org.apache.batik.transcoder.TranscoderInput; +import org.apache.batik.transcoder.print.PrintTranscoder; +import org.w3c.dom.Document; +import org.xhtmlrenderer.css.style.CalculatedStyle; +import org.xhtmlrenderer.layout.LayoutContext; +import org.xhtmlrenderer.pdf.ITextOutputDevice; +import org.xhtmlrenderer.pdf.ITextReplacedElement; +import org.xhtmlrenderer.render.BlockBox; +import org.xhtmlrenderer.render.PageBox; +import org.xhtmlrenderer.render.RenderingContext; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.print.PageFormat; +import java.awt.print.Paper; + +/** + * Represents an SVG element that is inlined into the PDF. + *

+ * The SVG is rendered into a PDF template via Apache Batik and then placed into the PDF. + * + * @see Stackoverflow + */ +public class InlinedSvgElement implements ITextReplacedElement { + + private final Point location = new Point(0, 0); + private final Document svg; + private final int cssWidth; + private final int cssHeight; + + protected InlinedSvgElement(Document svg, int cssWidth, int cssHeight) { + this.svg = svg; + this.cssWidth = cssWidth; + this.cssHeight = cssHeight; + } + + @Override + public void paint(RenderingContext renderingContext, ITextOutputDevice outputDevice, BlockBox blockBox) { + PdfContentByte contentByte = outputDevice.getWriter().getDirectContent(); + float width = cssWidth / outputDevice.getDotsPerPoint(); + float height = cssHeight / outputDevice.getDotsPerPoint(); + + Paper paper = new Paper(); + paper.setSize(width, height); + paper.setImageableArea(0, 0, width, height); + + PageFormat pageFormat = new PageFormat(); + pageFormat.setPaper(paper); + + PdfTemplate template = contentByte.createTemplate(width, height); + Graphics2D graphics = template.createGraphics(width, height); + PrintTranscoder printTranscoder = new PrintTranscoder(); + TranscoderInput transcoderInput = new TranscoderInput(svg); + printTranscoder.transcode(transcoderInput, null); + printTranscoder.print(graphics, pageFormat, 0); + graphics.dispose(); + + PageBox page = renderingContext.getPage(); + float x = (float) blockBox.getAbsX() + page.getMarginBorderPadding(renderingContext, CalculatedStyle.LEFT); + float y = (float) (page.getBottom() - (blockBox.getAbsY() + cssHeight)) + page.getMarginBorderPadding( + renderingContext, + CalculatedStyle.BOTTOM); + x /= outputDevice.getDotsPerPoint(); + y /= outputDevice.getDotsPerPoint(); + + contentByte.addTemplate(template, x, y); + } + + @Override + public int getIntrinsicWidth() { + return cssWidth; + } + + @Override + public int getIntrinsicHeight() { + return cssHeight; + } + + @Override + public Point getLocation() { + return location; + } + + @Override + public void setLocation(int x, int y) { + location.setLocation(x, y); + } + + @Override + public void detach(LayoutContext layoutContext) { + // nothing to do + } + + @Override + public boolean isRequiresInteractivePaint() { + return false; + } + + @Override + public boolean hasBaseline() { + return false; + } + + @Override + public int getBaseline() { + return 0; + } +}