Relative paths in Flying Saucer XHTML?
Asked Answered
C

7

9

I am using Flying Saucer to render some PDF documents from strings to XHTML. My code is something like:

iTextRenderer.setDocument(documentGenerator.generate(xhtmlDocumentAsString));
iTextRenderer.layout();
iTextRenderer.createPDF(outputStream);

What I'm trying to understand is, when using this method, where are relative paths in the XHTML resolved from? For example, for images or stylesheets. I am able to use this method to successfully generate a text-based document, but I need to understand how to reference my images and CSS.

Culet answered 4/3, 2010 at 10:9 Comment(0)
K
20

The setDocument() method takes two parameters: document and url. The url parameter indicates the base url used to prepend to relative paths that appear in the xhtml, such as in img tags.

Suppose you have:

<img src="images/img1.jpg">

Now suppose the folder "images" is located at:

C:/physical/route/to/app/images/

You may use setDocument() as:

renderer.setDocument(xhtmlDoc, "file:///C:/physical/route/to/app/");

Notice the trailing slash, it won't work without it.

This is the way it worked for me. I assume you could use other types of urls such as "http://...".

Kaitlin answered 7/1, 2011 at 17:25 Comment(1)
Specifying the file:// prefix is important because FS is looking for a URL, not a path. It won't work with just a path that starts with C:/ or /some/absolute/path.Sober
A
9

This week I worked on this, and I give you what worked fine for me.

In real life, your XHTML document points to multiple resources (images, css, etc.) with relative paths. You also have to explain to Flying Saucer where to find them. They can be in your classpath, or in your file system. (If they are on the network, you can just set the base url, so this won't help)

So you have to extend the ITextUserAgent like this:

private static class ResourceLoaderUserAgent extends ITextUserAgent {

    public ResourceLoaderUserAgent(ITextOutputDevice outputDevice) {
        super(outputDevice);
    }

    protected InputStream resolveAndOpenStream(String uri) {

        InputStream is = super.resolveAndOpenStream(uri);
        String fileName = "";
        try {
            String[] split = uri.split("/");
            fileName = split[split.length - 1];
        } catch (Exception e) {
            return null;
        }

        if (is == null) {
            // Resource is on the classpath
            try{
                is = ResourceLoaderUserAgent.class.getResourceAsStream("/etc/images/" + fileName);
            } catch (Exception e) {
        }

        if (is == null) {
            // Resource is in the file system
            try {
                is = new FileInputStream(new File("C:\\images\\" + fileName));
            } catch (Exception e) {
            }
        }

        return is;
    }
}

And you use it like this:

// Output stream containing the result
ByteArrayOutputStream baos = new ByteArrayOutputStream();

ITextRenderer renderer = new ITextRenderer();
ResourceLoaderUserAgent callback = new ResourceLoaderUserAgent(renderer.getOutputDevice());
callback.setSharedContext(renderer.getSharedContext());
renderer.getSharedContext().setUserAgentCallback(callback);

renderer.setDocumentFromString(htmlSourceAsString);

renderer.layout();
renderer.createPDF(baos);
renderer.finishPDF();

Cheers.

Aggregate answered 15/2, 2013 at 13:26 Comment(1)
What if the resource is password protected in Tomcat?Cybele
F
2

The best solution for me was:

renderer.setDocumentFromString(htmlContent,  new ClassPathResource("/META-INF/pdfTemplates/").getURL().toExternalForm());

Then all the provided styles and images in html (like

<img class="logo" src="images/logo.png" />
<link rel="stylesheet" type="text/css" media="all" href="css/style.css"></link>

) were rendered as expected.

Furlani answered 15/6, 2016 at 17:5 Comment(2)
This must be Spring based solution.Seethrough
Does this work if we have a RestController ? I am assuming that you are putting all CSS and image under the static directory.Fourth
S
1

AtilaUy's answer is spot-on for the default way things work in Flying Saucer.

The more general answer is that it asks the UserAgentContext. It will call setBaseURL() on the UserAgentContext when the document is set in. Then it will call resolveURL() to resolve relative URLs and ultimately resolveAndOpenStream() when it wants to read the actual resource data.

Well, this answer is probably way too late for you to make use of it anyway, but I needed an answer like this when I set out, and setting a custom user agent context is the solution I ended up using.

Service answered 19/4, 2011 at 3:22 Comment(0)
I
0

You can either have file paths, which should be absolute, or http:// urls. Relative paths can work but aren't portable because it depends on what directory you ran your program from

Intuition answered 16/3, 2011 at 15:27 Comment(0)
J
0

I think a easier approach would be:

                DomNodeList<DomElement> images = result.getElementsByTagName("img");
                for (DomElement e : images) { 
                    e.setAttribute("src", result.getFullyQualifiedUrl(e.getAttribute("src")).toString());
                }
Jujitsu answered 30/7, 2014 at 14:42 Comment(0)
M
0

Another way to resolve paths is to override UserAgentCallback#resolveURI, which offers a more dynamic behavior than a fixed URL (as in AtilaUy's answer, which looks quite valid for most cases).

This is how I make an XHTMLPane use dynamically-generated stylesheets:

public static UserAgentCallback interceptCssResourceLoading(
    final UserAgentCallback defaultAgentCallback,
    final Map< URI, CSSResource > cssResources
) {
  return new UserAgentCallback() {
    @Override
    public CSSResource getCSSResource( final String uriAsString ) {
      final URI uri = uriQuiet( uriAsString ) ; // Just rethrow unchecked exception.
      final CSSResource cssResource = cssResources.get( uri )  ;
      if( cssResource == null ) {
        return defaultAgentCallback.getCSSResource( uriAsString ) ;
      } else {
        return cssResource ;
      }
    }

    @Override
    public String resolveURI( final String uriAsString ) {
      final URI uri = uriQuiet( uriAsString ) ;
      if( cssResources.containsKey( uri ) ) {
        return uriAsString ;
      } else {
        return defaultAgentCallback.resolveURI( uriAsString ) ;
      }
    }

    // Delegate all other methods to defaultUserAgentCallback.

  } ;
}

Then I use it like that:

  final UserAgentCallback defaultAgentCallback =
      xhtmlPanel.getSharedContext().getUserAgentCallback() ;
  xhtmlPanel.getSharedContext().setUserAgentCallback(
      interceptCssResourceLoading( defaultAgentCallback, cssResources ) ) ;
  xhtmlPanel.setDocumentFromString( xhtml, null, new XhtmlNamespaceHandler() ) ;
Merrygoround answered 26/10, 2015 at 8:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.