Using Flying Saucer to Render Images to PDF In Memory
Asked Answered
L

5

21

I'm using Flying Saucer to convert XHTML to a PDF document. I've gotten the code to work with just basic HTML and in-line CSS, however, now I'm attempting to add an image as a sort of header to the PDF. What I'm wondering is if there is any way whatsoever to add the image by reading in an image file as a Java Image object, then adding that somehow to the PDF (or to the XHTML -- like it gets a virtual "url" representing the Image object that I can use to render the PDF). Has anyone ever done anything like this?

Thanks in advance for any help you can provide!

Lob answered 13/7, 2012 at 19:26 Comment(1)
There is a feature request to make data-url for images work directly in Flying Saucer: code.google.com/p/flying-saucer/issues/detail?id=202Ailurophile
H
41

I had to do that last week so hopefully I will be able to answer you right away.

Flying Saucer

The easiest way is to add the image you want as markup in your HTML template before rendering with Flying Saucer. Within Flying Saucer you will have to implement a ReplacedElementFactory so that you can replace any markup before rendering with the image data.

/**
 * Replaced element in order to replace elements like 
 * <tt>&lt;div class="media" data-src="image.png" /></tt> with the real
 * media content.
 */
public class MediaReplacedElementFactory implements ReplacedElementFactory {
    private final ReplacedElementFactory superFactory;

    public MediaReplacedElementFactory(ReplacedElementFactory superFactory) {
        this.superFactory = superFactory;
    }

    @Override
    public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockBox blockBox, UserAgentCallback userAgentCallback, int cssWidth, int cssHeight) {
        Element element = blockBox.getElement();
        if (element == null) {
            return null;
        }
        String nodeName = element.getNodeName();
        String className = element.getAttribute("class");
        // Replace any <div class="media" data-src="image.png" /> with the
        // binary data of `image.png` into the PDF.
        if ("div".equals(nodeName) && "media".equals(className)) {
            if (!element.hasAttribute("data-src")) {
                throw new RuntimeException("An element with class `media` is missing a `data-src` attribute indicating the media file.");
            }
            InputStream input = null;
            try {
                input = new FileInputStream("/base/folder/" + element.getAttribute("data-src"));
                final byte[] bytes = IOUtils.toByteArray(input);
                final Image image = Image.getInstance(bytes);
                final FSImage fsImage = new ITextFSImage(image);
                if (fsImage != null) {
                    if ((cssWidth != -1) || (cssHeight != -1)) {
                        fsImage.scale(cssWidth, cssHeight);
                    }
                    return new ITextImageElement(fsImage);
                }
            } catch (Exception e) {
                throw new RuntimeException("There was a problem trying to read a template embedded graphic.", e);
            } finally {
                IOUtils.closeQuietly(input);
            }
        }
        return this.superFactory.createReplacedElement(layoutContext, blockBox, userAgentCallback, cssWidth, cssHeight);
    }

    @Override
    public void reset() {
        this.superFactory.reset();
    }

    @Override
    public void remove(Element e) {
        this.superFactory.remove(e);
    }

    @Override
    public void setFormSubmissionListener(FormSubmissionListener listener) {
        this.superFactory.setFormSubmissionListener(listener);
    }
}

You will notice that I have hardcoded here /base/folder which is the folder where the HTML file is located as it will be the root url for Flying Saucer for resolving medias. You may change it to the correct location, coming from anywhere you want (Properties for example).

HTML

Within your HTML markup you indicate somewhere a <div class="media" data-src="somefile.png" /> like so:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>My document</title>
        <style type="text/css">
        #logo { /* something if needed */ }
        </style>
    </head>
    <body>
        <!-- Header -->
        <div id="logo" class="media" data-src="media/logo.png" style="width: 177px; height: 60px" />
        ...
    </body>
</html>

Rendering

And finally you just need to indicate your ReplacedElementFactory to Flying-Saucer when rendering:

String content = loadHtml();
ITextRenderer renderer = new ITextRenderer();
renderer.getSharedContext().setReplacedElementFactory(new MediaReplacedElementFactory(renderer.getSharedContext().getReplacedElementFactory()));
renderer.setDocumentFromString(content.toString());
renderer.layout();
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
renderer.createPDF(baos);
// baos.toByteArray();

I have been using Freemarker to generate the HTML from a template and then feeding the result to FlyingSaucer with great success. This is a pretty neat library.

Hydrophilic answered 13/7, 2012 at 19:59 Comment(2)
Hey @IcedDante it's been awhile since I had to do that. Cool to know that stuff still helps someone from time to time :)Hydrophilic
Excellent answer, for all those that want the image from a resources folder can just replace the FileInputStream with: input = this.getClass().getClassLoader().getResourceAsStream(element.getAttribute("data-src"));Verlinevermeer
U
11

what worked for me is putting it as a embedded image. So converting image to base64 first and then embed it:

    byte[] image = ...
    ITextRenderer renderer = new ITextRenderer();
    renderer.setDocumentFromString("<html>\n" +
                                   "    <body>\n" +
                                   "        <h1>Image</h1>\n" +
                                   " <div><img src=\"data:image/png;base64," + Base64.getEncoder().encodeToString(image) + "\"></img></div>\n" +
                                   "    </body>\n" +
                                   "</html>");
    renderer.layout();
    renderer.createPDF(response.getOutputStream());
Unhallowed answered 20/2, 2018 at 10:2 Comment(0)
B
7

Thanks Alex for detailed solution. I'm using this solution and found there is another line to be added to make it work.

public ReplacedElement createReplacedElement(LayoutContext layoutContext, BlockBox blockBox, UserAgentCallback userAgentCallback, int cssWidth, int cssHeight) {
  Element element = blockBox.getElement();
  ....
  ....
  final Image image = Image.getInstance(bytes);
  final int factor = ((ITextUserAgent)userAgentCallback).getSharedContext().getDotsPerPixel(); //Need to add this line
  image.scaleAbsolute(image.getPlainWidth() * factor, image.getPlainHeight() * factor) //Need to add this line
  final FSImage fsImage = new ITextFSImage(image);
  ....
  ....

We need to read the DPP from SharedContext and scale the image to display render the image on PDF.

Another suggestion: We can directly extend ITextReplacedElement instead of implementing ReplacedElementFactory. In that case we can set the ReplacedElementFactory in the SharedContext as follows:

renderer.getSharedContext().setReplacedElementFactory(new MediaReplacedElementFactory(renderer.getOutputDevice()); 
Bootlace answered 31/5, 2016 at 13:29 Comment(3)
I have applied scaling as per your suggestion (otherwise I was getting micro-sized images) BUT my perfect-looking PNG images appear with nasty artifacts in PDF output. In particular I see a thin irregular black border all around my ring-shaped charts. Any idea?Lingo
I have found the answer: PNG transparency is not well supported by PDF. See https://mcmap.net/q/658880/-itext-add-png-image-with-no-borderLingo
Thank you so much, after 5 years, this saved my day. Without this, the size of the embedded image become a lot smaller. I actually used the below lines. final int factor = ((ITextUserAgent) userAgentCallback).getSharedContext().getDotsPerPixel(); fsImage.scale(Math.round(image.getPlainWidth() * factor), Math.round(image.getPlainHeight() * factor));Tosha
A
0

@alex's solution is still true for

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-openpdf</artifactId>
    <version>9.1.22</version>
</dependency>
Abnormal answered 28/3, 2023 at 2:57 Comment(0)
G
0

If you have your image as a string, csviri's solution is your answer.

Else if you have your image in a folder, set its root path as the second argument baseUrl for setDocumentFromString(), then you'll be able to use relative paths in your HTML (achieving your "virtual url" effect):

String baseUrl = "file:/C:/Users/jtoland/workspace/myApp/myAppWebRootFolder/";
renderer.setDocumentFromString(html, baseUrl);

In a servlet you could do

renderer.setDocumentFromString(html, getServletContext().getResource("/").toString());

After that, not only images like <img src="img/image.png" /> but any resource like <link rel="stylesheet" href="css/style.css" /> will work with relative paths.

Gastrostomy answered 8/4, 2024 at 7:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.