Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline SVGs for PDFs (OX-9272) #1476

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}