Skip to content

Commit

Permalink
Merge pull request #1476 from scireum/feature/jvo/OX-9272-PDF-SVG
Browse files Browse the repository at this point in the history
Inline SVGs for PDFs (OX-9272)
  • Loading branch information
jakobvogel authored Nov 12, 2024
2 parents 3506856 + 4e29181 commit af1e24e
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 6 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@
<version>1.17.2</version>
</dependency>

<!-- Used to render SVG into PDF -->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-all</artifactId>
<version>1.18</version>
</dependency>

<!-- POI is used to generate excel exports -->
<dependency>
<groupId>org.apache.poi</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,18 +21,25 @@
import sirius.kernel.health.Exceptions;
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.
* <p>
* Alongside http different URI protocols are supported. These are handled by classes extending
* {@link PdfReplaceHandler}.
*/
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)
Expand All @@ -57,25 +65,60 @@ public ReplacedElement createReplacedElement(LayoutContext layoutContext,
return null;
}

return this.tryCreateReplacedImageElement(element, userAgentCallback, cssWidth, cssHeight)
.or(() -> this.tryCreateReplacedSvgElement(element, cssWidth, cssHeight))
.orElseGet(() -> super.createReplacedElement(layoutContext,
box,
userAgentCallback,
cssWidth,
cssHeight));
}

private Optional<ReplacedElement> 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<ReplacedElement> 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) {
Expand Down
120 changes: 120 additions & 0 deletions src/main/java/sirius/web/templates/pdf/InlinedSvgElement.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* The SVG is rendered into a PDF template via Apache Batik and then placed into the PDF.
*
* @see <a href="https://stackoverflow.com/questions/37056791/svg-integration-in-pdf-using-flying-saucer">Stackoverflow</a>
*/
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;
}
}

0 comments on commit af1e24e

Please sign in to comment.