使用iText将HTML转换为PDF

24

我发布这个问题是因为许多开发者以不同的形式提出了类似的问题。我将亲自回答这个问题(我是iText Group的创始人/CTO),以便它成为一个“维基答案”。如果Stack Overflow的“文档”功能仍然存在,那么这将是一个很好的文档主题候选。

源文件:

我正在尝试将以下HTML文件转换为PDF:

<html>
    <head>
        <title>Colossal (movie)</title>
        <style>
            .poster { width: 120px;float: right; }
            .director { font-style: italic; }
            .description { font-family: serif; }
            .imdb { font-size: 0.8em; }
            a { color: red; }
        </style>
    </head>
    <body>
        <img src="img/colossal.jpg" class="poster" />
        <h1>Colossal (2016)</h1>
        <div class="director">Directed by Nacho Vigalondo</div>
        <div class="description">Gloria is an out-of-work party girl
            forced to leave her life in New York City, and move back home.
            When reports surface that a giant creature is destroying Seoul,
            she gradually comes to the realization that she is somehow connected
            to this phenomenon.
        </div>
        <div class="imdb">Read more about this movie on
            <a href="www.imdb.com/title/tt4680182">IMDB</a>
        </div>
    </body>
</html>

在浏览器中,这个HTML看起来像这样:

enter image description here

我遇到的问题:

HTMLWorker完全不考虑CSS

当我使用HTMLWorker时,我需要创建一个ImageProvider以避免出现“找不到图像”的错误。我还需要创建一个StyleSheet实例来更改一些样式:

public static class MyImageFactory implements ImageProvider {
    public Image getImage(String src, Map<String, String> h,
            ChainedProperties cprops, DocListener doc) {
        try {
            return Image.getInstance(
                String.format("resources/html/img/%s",
                    src.substring(src.lastIndexOf("/") + 1)));
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }    
}

public static void main(String[] args) throws IOException, DocumentException {
    Document document = new Document();
    PdfWriter.getInstance(document, new FileOutputStream("results/htmlworker.pdf"));
    document.open();
    StyleSheet styles = new StyleSheet();   
    styles.loadStyle("imdb", "size", "-3");
    HTMLWorker htmlWorker = new HTMLWorker(document, null, styles);
    HashMap<String,Object> providers = new HashMap<String, Object>();
    providers.put(HTMLWorker.IMG_PROVIDER, new MyImageFactory());
    htmlWorker.setProviders(providers);
    htmlWorker.parse(new FileReader("resources/html/sample.html"));
    document.close();   
}

结果看起来像这样:

输入图像描述

由于某种原因,HTMLWorker还显示了<title>标签的内容。我不知道如何避免这种情况。头部中的 CSS 根本没有被解析, 我必须在代码中定义所有的样式使用 StyleSheet 对象。

当我查看我的代码时,我发现我正在使用的许多对象和方法已经被弃用:

输入图像描述

因此,我决定升级到使用 XML Worker。


在使用 XML Worker 时找不到图片

我尝试了以下代码:

public static final String DEST = "results/xmlworker1.pdf";
public static final String HTML = "resources/html/sample.html";
public void createPdf(String file) throws IOException, DocumentException {
    Document document = new Document();
    PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(file));
    document.open();
    XMLWorkerHelper.getInstance().parseXHtml(writer, document,
            new FileInputStream(HTML));
    document.close();
}

这导致产生了以下的PDF文件:

enter image description here

默认字体Helvetica被用来代替Times-Roman;这是iText的典型特征(我应该在HTML中显式定义字体)。除此之外,CSS似乎被正确处理,但图片丢失了,而我也没有收到错误信息。

使用HTMLWorker时,会抛出一个异常,通过引入ImageProvider,我成功解决了这个问题。现在我们看看是否适用于XML Worker。

并非所有的CSS样式都受到XML Worker的支持

我修改了我的代码如下:

public static final String DEST = "results/xmlworker2.pdf";
public static final String HTML = "resources/html/sample.html";
public static final String IMG_PATH = "resources/html/";
public void createPdf(String file) throws IOException, DocumentException {
    Document document = new Document();
    PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(file));
    document.open();

    CSSResolver cssResolver =
            XMLWorkerHelper.getInstance().getDefaultCssResolver(true);
    HtmlPipelineContext htmlContext = new HtmlPipelineContext(null);
    htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory());
    htmlContext.setImageProvider(new AbstractImageProvider() {
        public String getImageRootPath() {
            return IMG_PATH;
        }
    });

    PdfWriterPipeline pdf = new PdfWriterPipeline(document, writer);
    HtmlPipeline html = new HtmlPipeline(htmlContext, pdf);
    CssResolverPipeline css = new CssResolverPipeline(cssResolver, html);

    XMLWorker worker = new XMLWorker(css, true);
    XMLParser p = new XMLParser(worker);
    p.parse(new FileInputStream(HTML));

    document.close();
}

代码长度增加了,但是现在图像被渲染出来了:

输入图像描述

这张图片比我使用HTMLWorker渲染时要大, 这告诉我类poster的CSS属性width 被考虑进去了,但是float属性被忽略了。我该如何解决这个问题?

剩下的问题:

所以问题归结为这样: 我有一个特定的HTML文件,我想把它转换成PDF。我经历了很多的工作,一个接一个地解决问题,但有一个特定的问题我无法解决:如何让iText尊重定义元素位置(例如float:right)的CSS?

额外的问题:

当我的HTML包含表单元素(如<input>),这些表单元素会被忽略。


5
为什么该问题会被标记为“过于宽泛”?这是一个非常具体的问题,提供了非常具体的例证。我添加了源代码,导致了一些无法使用旧版 iText 功能解决的非常具体的问题。答案表明,使用新版本可以充分地解决问题。你可能会认为有重复的问题(我很容易找到几十个重复的问题),但是那些重复的问题不够具体,并且我们确实需要一个参考答案。现在 SO 的文档已经荒废不堪,这是唯一的方式来发布一个好的问题和一个好的答案。 - Bruno Lowagie
4
因为没有一个具体的问题。这里未被问到的“问题”是,我如何将这个HTML转换成PDF。整个问题太过宽泛。而且总体风格似乎不太适合SO的格式,这不是文档。 - jmoerdyk
好的,我会添加那个问题。虽然它在结尾处说“我该如何解决这个问题?”,但这个问题展示了一条明确的解决路径,只有一个无法解决的事情(使用CSS进行绝对定位)。 - Bruno Lowagie
2
这个问题已经在以下链接的评论中有很多参考:https://stackoverflow.com/questions/47872246 https://stackoverflow.com/questions/47852780 https://stackoverflow.com/questions/47830668 https://stackoverflow.com/questions/47787253 https://stackoverflow.com/questions/47808275 我需要添加多少个问题才能说服您这些问答对于大家都是有用的呢?如果我无法说服您,请重新引入SO文档功能,以便我可以将此内容添加为文档主题(或提供替代解决方案)。 - Bruno Lowagie
1
我已不再与任何iText公司有所关联。请查看我的LinkedIn个人资料获取更多信息。 - Bruno Lowagie
你能看到这个参考链接吗:https://stackoverflow.com/questions/77355363/lambda-spring-boot-converting-html-to-pdf 吗?谢谢 - undefined
3个回答

25

为什么你的代码不能工作

正如HTML转PDF教程中所解释的那样,HTMLWorker已经多年前被弃用。它并不打算将完整的HTML页面转换过来。它不知道一个HTML页面有一个<head>和一个<body>部分;它只是解析所有内容。它的目的是解析小的HTML片段,并且您可以使用StyleSheet类定义样式;真正的CSS是不支持的。

然后出现了XML Worker。XML Worker旨在作为解析XML的通用框架。作为概念验证,我们决定编写一些XHTML到PDF的功能,但我们并没有支持所有的HTML标签。例如:根本不支持表单,并且很难支持用于定位内容的CSS。HTML中的表单与PDF中的表单非常不同。此外,iText的架构与HTML + CSS的架构之间存在不匹配。逐渐地,我们扩展了XML Worker,主要基于客户的请求,但XML Worker成为了一个拥有许多触角的怪物。

最终,我们决定从头开始重写iText,考虑到HTML + CSS转换的要求。这导致了iText 7的产生。在iText 7的基础上,我们创建了几个附加组件,其中在这个上下文中最重要的是pdfHTML

如何解决问题

使用最新版本的iText(iText 7.1.0 + pdfHTML 2.0.0),将问题中的HTML转换为PDF的代码可简化为以下片段:

public static final String SRC = "src/main/resources/html/sample.html";
public static final String DEST = "target/results/sample.pdf";
public void createPdf(String src, String dest) throws IOException {
    HtmlConverter.convertToPdf(new File(src), new File(dest));
}

结果如下:

enter image description here

正如您所看到的,这几乎是您预期的结果。自iText 7.1.0 / pdfHTML 2.0.0以来,默认字体为Times-Roman。CSS正在被尊重:图像现在浮动在右侧。

一些额外的想法。

当我建议升级到iText 7 / pdfHTML 2时,开发人员经常感到反对升级到新版本的iText。请允许我回答我听到的前3个争论:

我需要使用免费的iText,而iText 7不是免费的/ pdfHTML插件是闭源的。

iText 7使用AGPL发布,就像iText 5和XML Worker一样。在开源项目的背景下,AGPL允许“免费使用”(即“免费”)。如果您正在分发闭源/专有产品(例如,在SaaS上下文中使用iText),则不能免费使用iText;在这种情况下,您必须购买商业许可证。iText 5已经如此,iText 7仍然如此。至于iText 5之前的版本:根本不应该使用这些。关于pdfHTML:最初版本确实只作为闭源软件提供。我们在iText Group内进行了激烈的讨论:一方面,有人希望避免由那些不听从开发人员建议的公司滥用开源的情况。开发人员告诉我们,他们的老板强迫他们做错误的事情,而他们无法说服老板购买商业许可证。另一方面,有人认为我们不应该因老板的错误行为而惩罚开发人员。最终,支持开源pdfHTML的人,即iText的开发人员,赢得了争论。请证明他们没有错,并正确使用iText:如果您免费使用iText,请尊重AGPL;如果您在闭源环境中使用iText,请确保您的老板购买商业许可证。

我需要维护一个旧系统,并且必须使用旧版本的iText。

真的吗?维护还涉及应用升级和迁移到使用的软件的新版本。正如您所看到的,使用iText 7和pdfHTML时所需的代码非常简单,比以前所需的代码更少容易出错。迁移项目不应该太长。

我刚刚开始,并不知道iText 7;只有在完成我的项目后才发现。

这就是为什么我发布这个问题和答案的原因。把自己想象成一个极限编程员。扔掉你所有的代码,重新开始。你会发现它并不像你想象的那么难,而且你会睡得更好,因为iText 5正在逐步淘汰。我们仍然为付费客户提供支持,但最终,我们将停止对iText 5的支持。


不错的发现,@mkl,文本说的是与我的意思相反的。 - Bruno Lowagie
1
@DanielASathishKumar "HTML文件大小接近1GB"。哇,我希望您不仅指的是HTML本身的大小,还包括其中链接的图像和文件的大小。我想知道您是否真的需要那些嵌入在PDF中的巨大文件。也许可以考虑使用公司/公共服务器上的链接? - Cleptus
@DanielASathishKumar 如果记录超过40,000条,则先将这些记录转换为HTML,然后创建PDF是错误的设计选择。请向您的经理提出更换提出此设计的架构师。 - Bruno Lowagie
FYI,这个功能即使在格式不良的HTML中也能很好地工作 - 它不会像itext5一样破坏转换。 - dalcam
@BrunoLowagie 你好,Bruno。我现在正在使用itext7.pdfhtml,但是pdfhtml dll无法识别一堆CSS属性。我的源代码在这个链接中:https://gofile.io/d/vQqULH - hosein
显示剩余7条评论

7
使用iText 7和以下代码:
public void generatePDF(String htmlFile) {
    try {

        //HTML String
        String htmlString = htmlFile;
        //Setting destination 
        FileOutputStream fileOutputStream = new FileOutputStream(new File(dirPath + "/USER-16-PF-Report.pdf"));
        
        PdfWriter pdfWriter = new PdfWriter(fileOutputStream);
        ConverterProperties converterProperties = new ConverterProperties();
        PdfDocument pdfDocument = new PdfDocument(pdfWriter);

        //For setting the PAGE SIZE
        pdfDocument.setDefaultPageSize(new PageSize(PageSize.A3));
        
        Document document = HtmlConverter.convertToDocument(htmlFile, pdfDocument, converterProperties);
        document.close();
    } 
    catch (Exception e) {
         e.printStackTrace();
    }
}

这些类com.itextpdf.html2pdf.ConverterProperties和com.itextpdf.html2pdf.HtmlConverter来自maven artifact com.itextpdf:html2pdf。 - Krigl Wurzl

0

将静态HTML页面转换为任何CSS样式:

 HtmlConverter.convertToPdf(new File("./pdf-input.html"),new File("demo-html.pdf"));

对于Spring Boot用户:使用SpringBoot和Thymeleaf将动态HTML页面转换:

    @RequestMapping(path = "/pdf")
    public ResponseEntity<?> getPDF(HttpServletRequest request, HttpServletResponse response) throws IOException {
    /* Do Business Logic*/

    Order order = OrderHelper.getOrder();

    /* Create HTML using Thymeleaf template Engine */

    WebContext context = new WebContext(request, response, servletContext);
    context.setVariable("orderEntry", order);
    String orderHtml = templateEngine.process("order", context);

    /* Setup Source and target I/O streams */

    ByteArrayOutputStream target = new ByteArrayOutputStream();
    ConverterProperties converterProperties = new ConverterProperties();
    converterProperties.setBaseUri("http://localhost:8080");
    /* Call convert method */
    HtmlConverter.convertToPdf(orderHtml, target, converterProperties);

    /* extract output as bytes */
    byte[] bytes = target.toByteArray();


    /* Send the response as downloadable PDF */

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=order.pdf")
            .contentType(MediaType.APPLICATION_PDF)
            .body(bytes);

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接