Using Data URLs for embedding images in Flying Saucer generated PDFs

24 / Aug / 2012 by Vivek Krishna 7 comments

We extensively use Flying Saucer to generate PDFs from GSPs in our grails applications. However, there is always the issue of embedding images from within the application because the URLs are usually relative to the environment and as such, embedding them in PDFs with a URL in the src attribute is cumbersome.

To get around this, we decided to write our own implementation of the ReplacedElementFactory taking some help from this excellent snippets of code. However, we didn’t find a need to go with our custom implementation of Base64 encoding and as such, we ended up using the sun.misc.BASE64Decoder. The resulting class looked like this :


import com.lowagie.text.BadElementException
import com.lowagie.text.Image
import org.w3c.dom.Element
import org.xhtmlrenderer.extend.FSImage
import org.xhtmlrenderer.extend.ReplacedElement
import org.xhtmlrenderer.extend.ReplacedElementFactory
import org.xhtmlrenderer.extend.UserAgentCallback
import org.xhtmlrenderer.layout.LayoutContext
import org.xhtmlrenderer.pdf.ITextFSImage
import org.xhtmlrenderer.pdf.ITextImageElement
import org.xhtmlrenderer.render.BlockBox
import org.xhtmlrenderer.simple.extend.FormSubmissionListener

public class B64ImgReplacedElementFactory implements ReplacedElementFactory {

 public ReplacedElement createReplacedElement(LayoutContext c, BlockBox box, UserAgentCallback uac, int cssWidth, int cssHeight) {
     Element e = box.getElement();
     if (e == null) {
         return null;
     }
     String nodeName = e.getNodeName();
     if (nodeName.equals("img")) {
         String attribute = e.getAttribute("src");
         FSImage fsImage;
         try {
             fsImage = buildImage(attribute, uac);
         } catch (BadElementException e1) {
             fsImage = null;
         } catch (IOException e1) {
             fsImage = null;
         }
         if (fsImage != null) {
             if (cssWidth != -1 || cssHeight != -1) {
                 fsImage.scale(cssWidth, cssHeight);
             }
             return new ITextImageElement(fsImage);
         }
     }
     return null;
 }

 protected FSImage buildImage(String srcAttr, UserAgentCallback uac) throws IOException, BadElementException {
      FSImage fsImage;
      if (srcAttr.startsWith("data:image/")) {
         String b64encoded = srcAttr.substring(srcAttr.indexOf("base64,") + "base64,".length(), srcAttr.length());
         byte[] decodedBytes = new sun.misc.BASE64Decoder().decodeBuffer(b64encoded);
         fsImage = new ITextFSImage(Image.getInstance(decodedBytes));
      } else {
         fsImage = uac.getImageResource(srcAttr).getImage();
      }
      return fsImage;
 }

 public void remove(Element e) {
 }

 public void reset() {
 }

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

Now, in the code where we call the Renderer, we used :

 byte[] generatePdf(String content) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] bytes = null
        ITextRenderer renderer = new ITextRenderer();
        SharedContext sharedContext = renderer.getSharedContext();
        sharedContext.setPrint(true);
        sharedContext.setInteractive(false);
        sharedContext.setReplacedElementFactory(new B64ImgReplacedElementFactory());
        sharedContext.getTextRenderer().setSmoothingThreshold(0);
        try {
            renderer.setDocumentFromString(content);
            renderer.layout();
            renderer.createPDF(byteArrayOutputStream);
            bytes = byteArrayOutputStream.toByteArray()
        }
        catch (Throwable e) {
            log.error("Error while generating pdf ${e.message}", e)
        }
        return bytes
    }

Now, we can embed images in the form of data URLs in our GSPs using base64 encoded version of the image bytes.

FOUND THIS USEFUL? SHARE IT

comments (7)

  1. Elizabeth

    I’m having the same problem with tiny images – how did you set the CSS width explicitly on the Image? I can only see the methods – scaleAbsolute(), scaleToFit() etc. Many thanks.

    Reply
  2. John

    Thanks. It solves my problem. One more help needed.

    I have a image as byte array. I do not want to get this image as base64 and decode it to get the byte array again for FSImage. Is there a way to directly pass byte array to createReplacedElement method and do the image replacement?

    Thanks.

    Reply
  3. Vidar S. Ramdal

    George; I also had those tiny images, but the problem went away when I explicitly set CSS width and height on the image elements:

    Hope this helps.

    Reply

Leave a comment -