Render HTML with SVG to PDF with Flying Saucer

August 13th 2013 by Samuel Rossille

With it's ability to render XHTML and CSS to PDF, Flying Saucer can be a very handy tool when you have to generate reports, especially in the context or a web application. It comes with a lot of benefits:

Overview of the solution

The solution involves the following libraries:

Implementation details

The ReplacedElementFactory interface

Flying Saucer allows to contribute to the PDF rendering process by defining the rendering process for some block elements with a low-level API of itextpdf. This is done by providing a custom implementation oforg.xhtmlrenderer.extend.ReplacedElementFactory and setting it as the replacedElementFactory to use in the global context.

renderer.getSharedContext().setReplacedElementFactory(replacedElementFactory);

The contract of the ReplacedElementFactory is to receive:

The ChainingReplacedElementFactory

The API of setReplacedElementFactory doesn't provide a way to contribute, and we don't know :

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.w3c.dom.Element;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.ReplacedElementFactory;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;

public class ChainingReplacedElementFactory implements ReplacedElementFactory {
    private List<ReplacedElementFactory> replacedElementFactories 
            = new ArrayList<ReplacedElementFactory>();

    public void addReplacedElementFactory(ReplacedElementFactory replacedElementFactory) {
        replacedElementFactories.add(0, replacedElementFactory);
    }

    @Override
    public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, 
            UserAgentCallback uac, int cssWidth, int cssHeight) {
        for(ReplacedElementFactory replacedElementFactory : replacedElementFactories) {
            ReplacedElement element = replacedElementFactory
                    .createReplacedElement(c, box, uac, cssWidth, cssHeight);
            if(element != null) {
                return element;
            }
        }
        return null;
    }

    @Override
    public void reset() {
        for(ReplacedElementFactory replacedElementFactory : replacedElementFactories) {
            replacedElementFactory.reset();
        }
    }

    @Override
    public void remove(Element e) {
        for(ReplacedElementFactory replacedElementFactory : replacedElementFactories) {
            replacedElementFactory.remove(e);
        }
    }

    @Override
    public void setFormSubmissionListener(FormSubmissionListener listener) {
        for(ReplacedElementFactory replacedElementFactory : replacedElementFactories) {
            replacedElementFactory.setFormSubmissionListener(listener);
        }
    }
}

Sample use:

ChainingReplacedElementFactory chainingReplacedElementFactory 
        = new ChainingReplacedElementFactory();
chainingReplacedElementFactory.addReplacedElementFactory(replacedElementFactory);
chainingReplacedElementFactory.addReplacedElementFactory(new SVGReplacedElementFactory());
renderer.getSharedContext().setReplacedElementFactory(chainingReplacedElementFactory);

The SVGReplacedElementFactory

If we refer to the generic contract of the ReplacedElementFactory what we have to do here is to implement the contract and return an SVGReplacedElement (see later) for SVG elements, and null for any other element. The main job of the implementation below is to export the SVG nodes from the XHTML document to a new document that will be required to perform the low level operations in the SVGReplacedElement

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xhtmlrenderer.extend.ReplacedElement;
import org.xhtmlrenderer.extend.ReplacedElementFactory;
import org.xhtmlrenderer.extend.UserAgentCallback;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.render.BlockBox;
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;

public class SVGReplacedElementFactory implements ReplacedElementFactory {
    @Override
    public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, 
            UserAgentCallback uac, int cssWidth, int cssHeight) {
        Element element = box.getElement();
        if("svg".equals(element.getNodeName())) {

            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder;

            try {
                documentBuilder = documentBuilderFactory.newDocumentBuilder();
            } catch (ParserConfigurationException e) {
                throw new RuntimeException(e);
            }
            Document svgDocument = documentBuilder.newDocument();
            Element svgElement = (Element) svgDocument.importNode(element, true);
            svgDocument.appendChild(svgElement);
            return new SVGReplacedElement(svgDocument, cssWidth, cssHeight);
        }
        return null;
    }

    @Override
    public void reset() {
    }

    @Override
    public void remove(Element e) {
    }

    @Override
    public void setFormSubmissionListener(FormSubmissionListener listener) {
    }
}

The SVGReplacedElement

Now we have to convert the SVG document that we created in the SVGReplacedElementFactory into low-level PDF object. This is where batik's transcoder helps us with it's ability to draw an SVG document with the 2D drawing API of a java.awt.Graphics2D, which is then consumed by itextpdf

import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
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 com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfTemplate;

public class SVGReplacedElement implements ITextReplacedElement {

    private Point location = new Point(0, 0);
    private Document svg;
    private int cssWidth;
    private int cssHeight;

    public SVGReplacedElement(Document svg, int cssWidth, int cssHeight) {
        this.cssWidth = cssWidth;
        this.cssHeight = cssHeight;
        this.svg = svg;
    }

    @Override
    public void detach(LayoutContext c) {
    }

    @Override
    public int getBaseline() {
        return 0;
    }

    @Override
    public int getIntrinsicWidth() {
        return cssWidth;
    }

    @Override
    public int getIntrinsicHeight() {
        return cssHeight;
    }

    @Override
    public boolean hasBaseline() {
        return false;
    }

    @Override
    public boolean isRequiresInteractivePaint() {
        return false;
    }

    @Override
    public Point getLocation() {
        return location;
    }

    @Override
    public void setLocation(int x, int y) {
        this.location.x = x;
        this.location.y = y;
    }

    @Override
    public void paint(RenderingContext renderingContext, ITextOutputDevice outputDevice, 
            BlockBox blockBox) {
        PdfContentByte cb = outputDevice.getWriter().getDirectContent();
        float width = (float) (cssWidth / outputDevice.getDotsPerPoint());
        float height = (float) (cssHeight / outputDevice.getDotsPerPoint());

        PdfTemplate template = cb.createTemplate(width, height);
        Graphics2D g2d = template.createGraphics(width, height);
        PrintTranscoder prm = new PrintTranscoder();
        TranscoderInput ti = new TranscoderInput(svg);
        prm.transcode(ti, null);
        PageFormat pg = new PageFormat();
        Paper pp = new Paper();
        pp.setSize(width, height);
        pp.setImageableArea(0, 0, width, height);
        pg.setPaper(pp);
        prm.print(g2d, pg, 0);
        g2d.dispose();

        PageBox page = renderingContext.getPage();
        float x = blockBox.getAbsX() + page.getMarginBorderPadding(renderingContext, CalculatedStyle.LEFT);
        float y = (page.getBottom() - (blockBox.getAbsY() + cssHeight)) + page.getMarginBorderPadding(
                renderingContext, CalculatedStyle.BOTTOM);
        x /= outputDevice.getDotsPerPoint(); 
        y /= outputDevice.getDotsPerPoint();

        cb.addTemplate(template, x, y);
    }
}

Comments

Amit: Hi, I am trying the same with your example above. But, I am not getting the graph in the pdf. I am trying to log inside "createReplacedElement" method. But the svgDocument is showing null. Also, "paint" method in SVGReplacedElement class is not getting called. How does it call that method? sorry for my limited knowledge. Thanks, Amit

alberto saez torres: To display the content you need to add a "style" element with the x,y, width, height and display:block (....) The block is necessary to ensure fs creates the reder box. with a few changes in SVGReplacedElementFactory the with and heigh parameters could be readed from the element instead from the style. Said that, I'm unable to get any image in my pdf.....

Rajasekhar: Hi, can you actually elaborate your article by giving an example of how to generate the pdf from an html document? I am not able to generate pdf with svg. The following link consists of what I have tried so far. http://stackoverflow.com/questions/24491917/how-to-create-a-pdf-document-with-the-following-html-string-with-svg