Supplying resources directly to Flying Saucer

Posted on July 28, 2019
Tags: programming, java, pdf

In the Java world, one of the options you have for generating PDFs is Flying Saucer. It is quite a nice solution that renders HTML to, amongst other things, PDF.

In its simplest form a report, receipt, etc, could just be a chunk of text. But, more realistic situations would require images to be embedded in the output, be these logos, charts, images, etc.

A commonly found suggestion for achieving this is to link to the assets via the file-system. This actually occurs by default if you set the base URL of the document to null:

NativeUserAgent.java:

    public String resolveURI(String uri) {
        if (uri == null) return null;

        if (_baseURL == null) {//first try to set a base URL
            try {
                URI result = new URI(uri);
                if (result.isAbsolute()) setBaseURL(result.toString());
            } catch (URISyntaxException e) {
                XRLog.exception("The default NaiveUserAgent could not use the URL as base url: " + uri, e);
            }
            if (_baseURL == null) { // still not set -> fallback to current working directory
                try {
                    setBaseURL(new File(".").toURI().toURL().toExternalForm());
                    ...

The main issues here become:

  1. This only works for static assets
  2. It seems fragile. e.g., will the base URL calculation work properly in an IDE as it does in the final packaged environment.

A better solution, in my opinion, is to take responsibility for resolving these assets. This allows static assets to be packaged on the class path and resolved reliably, and also to resolve dynamic assets.

The actual resolution of assets (including images and CSS) is provided by the UserAgentCallback interface, of which the default implementation is ITextUserAgent (sic). The different asset type resolutions all rely on a method protected InputStream resolveAndOpenStream(String uri) to provide the actual input stream, so by subclassing ITextUserAgent and replacing this method with some additional logic we can pick out the resources we wish to provide. Then, we simply instanciate the renderer with our new user class.


    byte[] render(String template) {
        ITextOutputDevice outputDevice = new ITextOutputDevice(ITextRenderer.DEFAULT_DOTS_PER_POINT);
        final ITextRenderer renderer = new ITextRenderer(ITextRenderer.DEFAULT_DOTS_PER_POINT,
                ITextRenderer.DEFAULT_DOTS_PER_PIXEL, outputDevice,
                new CustomTextUserAgent(outputDevice));
        renderer.setDocumentFromString(template, "file:/");
        renderer.layout();
        try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) {
            renderer.createPDF(fos);
            return fos.toByteArray();
        }
    }
...
class CustomTextUserAgent extends ITextUserAgent {
    public CustomTextUserAgent(ITextOutputDevice outputDevice) {
        super(outputDevice);
    }

    @Override
    protected InputStream resolveAndOpenStream(String uri) {
        if (uri.startsWith("file:")) {
            String path = uri.substring("file:".length());

            InputStream is = getClass().getClassLoader().getResourceAsStream(String.format("reports%s", path));
            if (is != null) {
                return is;
            }
        }
        return super.resolveAndOpenStream(uri);
    }
}

In the above example I’ve opted to use file:/ as the base URL for the document, which means that any relative reference will have this prefixed. Probably a better solution would be to come up with a custom URL prefix, such as resource:, but I’ve not tested this.

Now, if we were to have a document that contained, for instance:


<img src="logo.png">

this would be resolved as file:/logo.png, which would be then looked up against the class path reports/logo.png.

The next level of complexity would be to also have known URLs to generated resources, that could call JFreeChart, or resolve the logo depending on the context or filename to get the content from the correct source.