使用飞碟将图像渲染为内存中的 PDF

Posted

技术标签:

【中文标题】使用飞碟将图像渲染为内存中的 PDF【英文标题】:Using Flying Saucer to Render Images to PDF In Memory 【发布时间】:2012-07-13 16:37:19 【问题描述】:

我正在使用 Flying Saucer 将 Xhtml 转换为 PDF 文档。我已经让代码只使用基本的 HTML 和内联 CSS,但是,现在我正在尝试将图像作为一种标题添加到 PDF 中。我想知道是否有任何方法可以通过将图像文件作为Java Image 对象读取来添加图像,然后以某种方式将其添加到PDF(或XHTML——就像它得到一个虚拟的“url”一样)表示我可以用来呈现 PDF 的 Image 对象)。有没有人做过这样的事情?

提前感谢您提供的任何帮助!

【问题讨论】:

有一个功能要求让图片的 data-url 直接在飞碟中工作:code.google.com/p/flying-saucer/issues/detail?id=202 【参考方案1】:

上周我不得不这样做,所以希望我能马上回答你。

飞碟

最简单的方法是在使用 Flying Saucer 渲染之前,在 HTML 模板中添加您想要的图像作为标记。在 Flying Saucer 中,您必须实现 ReplacedElementFactory,以便您可以在使用图像数据进行渲染之前替换任何标记。

/**
 * 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);
    

你会注意到我在这里硬编码了/base/folder,这是HTML文件所在的文件夹,因为它将是Flying Saucer解析媒体的根URL。您可以将其更改为正确的位置,来自您想要的任何位置(例如属性)。

HTML

在您的 HTML 标记中,您在某处指出 &lt;div class="media" data-src="somefile.png" /&gt;,如下所示:

<?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>

渲染

最后你只需要在渲染时向飞碟表明你的ReplacedElementFactory

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();

我一直在使用 Freemarker 从模板生成 HTML,然后将结果提供给 FlyingSaucer,并取得了巨大的成功。这是一个非常整洁的库。

【讨论】:

嘿@IcedD​​ante 我已经有一段时间没有这样做了。很高兴知道这些东西仍然不时帮助某人:) 很好的答案,对于所有想要从资源文件夹中获取图像的人,只需将 FileInputStream 替换为: input = this.getClass().getClassLoader().getResourceAsStream(element.getAttribute("data- src"));【参考方案2】:

对我有用的是将它作为嵌入图像。所以先把图片转base64再嵌入:

    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());

【讨论】:

【参考方案3】:

感谢 Alex 提供详细的解决方案。我正在使用这个解决方案,发现还需要添加另一行以使其正常工作。

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);
  ....
  ....

我们需要从SharedContext 读取 DPP 并缩放图像以在 PDF 上显示渲染图像。

另一个建议: 我们可以直接扩展ITextReplacedElement,而不是实现ReplacedElementFactory。在这种情况下,我们可以在SharedContext 中设置ReplacedElementFactory,如下所示:

renderer.getSharedContext().setReplacedElementFactory(new MediaReplacedElementFactory(renderer.getOutputDevice()); 

【讨论】:

我已根据您的建议应用了缩放(否则我得到的是微型图像)但我看起来完美的 PNG 图像在 PDF 输出中出现了令人讨厌的伪影。特别是,我在环形图表周围看到了一条不规则的细黑色边框。有什么想法吗? 我找到了答案:PDF 不能很好地支持 PNG 透明度。见***.com/a/34506685/685806

以上是关于使用飞碟将图像渲染为内存中的 PDF的主要内容,如果未能解决你的问题,请参考以下文章

使用 jinja2 和 weasyprint 在 PDF 渲染中忽略图像

mPDF 将图像渲染为翻转并隐藏在容器中

如何在 Android 中渲染 PDF

将 PDF 转换为 PNG

使用 html-pdf-node 渲染的 pdf 中未显示 CSS 和图像

为啥通过 ghostscript API 渲染图像需要这么多时间?