flyEn'blog

PDF报告生成方案

期望:能够通过编写某种模板,把PDF的大概样子确定下来,然后把数据和模板做一次整合,得到最终的结果,生成PDF导出。
最终方案:freemarker + flying-saucer-pdf + iText

iText是著名的开放源码的站点sourceforge一个项目,是用于生成PDF文档的一个java类库。通过iText不仅可以生成PDF或rtf的文档,而且可以将XML、Html文件转化为PDF文件。但iText html转PDF对中文和css的支持都不是很好,后来调研到了flying-saucer-pdf这个工具库,用它生成pdf解决了大部分的问题。它依赖于iText实现。

  1. 图片显示问题。
  2. 中文显示问题,css样式问题。
  3. 表格跨行问题。

关于Flying Saucer

Flying Saucer是一个纯Java开源项目库,它使用CSS2.1进行布局渲染呈现格式良好的XML或XHTML,导出到Swing面板、PDF或图像。

New releases of Flying Saucer are distributed through Maven. The available artifacts are:
org.xhtmlrenderer:flying-saucer-core - Core library and Java2D rendering
org.xhtmlrenderer:flying-saucer-pdf - PDF output using iText 2.x
org.xhtmlrenderer:flying-saucer-pdf-itext5 - PDF output using iText 5.x
org.xhtmlrenderer:flying-saucer-pdf-openpdf - PDF output using OpenPDF
org.xhtmlrenderer:flying-saucer-swt - SWT output
org.xhtmlrenderer:flying-saucer-log4j - Logging plugin for log4j

GitHub:https://github.com/flyingsaucerproject/flyingsaucer

流程实现:

1. build.gradle

1
2
compile('org.freemarker:freemarker:2.3.28')
compile('org.xhtmlrenderer:flying-saucer-pdf:9.1.16')

2. 编写Freemarker模板(或者使用其他模板引擎),打造HTML,绘制出PDF的模板。

  • css渲染
    用css控制- ①图片、PDF模块防断裂 ②PDF纸张大小、方向、换页、page模型边距设置

分页媒体格式模型中,文档被转移到一个或多个页面框。
该页框是映射到一个矩形平面。
这大致类似于css盒子模型:
Alt text
CSS Paged Media Module Level 3

3. 引入Freemarker的引擎,将数据和模板使用引擎生成最终的内容(htmlStr)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* freemarker 引擎渲染 html
*
* @param dataMap 传入 html 模板的 Map 数据
* @param ftlDir html 模板文件存放包的相对路径(相对于resources路径的包路径) eg: /templates
* @param ftlFile html 模板文件名 eg: pdf_export_demo.ftl
*
* @return
*/
public static String freemarkerRender(Object dataMap, String ftlDir, String ftlFile) {
Writer out = new StringWriter();
configuration.setDefaultEncoding("UTF-8");
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
try {
configuration.setClassForTemplateLoading(PDFUtil.class, ftlDir);
configuration.setLogTemplateExceptions(false);
configuration.setWrapUncheckedExceptions(true);
Template template = configuration.getTemplate(ftlFile);
template.process(dataMap, out);
out.flush();
return out.toString();
} catch (IOException | TemplateException e) {
e.printStackTrace();
log.error("pdf模板资源文件载入失败", e);
throw new IllegalArgumentException("pdf模板资源文件载入失败", e);
}
}

关于资源文件路径问题
用类加载器去找资源文件下的路径
其实这个方法是根据类加载路径来判断的,最终会执行以下代码:

1
FreemarkerUtil.class.getClassLoader().getResource("/template/");

这里注意一下第二个参数需要以 "/"开头。
Freemarker提供了3种加载模板目录的方法。 它使用Configuration类加载模板:
3种方法分别是:

  1. public void setClassForTemplateLoading(Class clazz, String pathPrefix); –基于类路径
  2. public void setDirectoryForTemplateLoading(File dir) throws IOException; –基于文件系统
  3. public void setServletContextForTemplateLoading(Object servletContext, String path); –Servlet Context

4. 利用ITextRenderer解析生成的HTML模板,创建PDF。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 使用 iText 生成 PDF 文档
*
* @param htmlTmpStr html 模板文件字符串
* @param fontFile 所需字体文件(相对路径+文件名)
*/
public static byte[] createPDF(String htmlTmpStr, String fontFile) {
ByteArrayOutputStream outputStream;
byte[] result;
try {
outputStream = new ByteArrayOutputStream();
ITextFontResolver fontResolver = renderer.getFontResolver();

// 解决中文支持问题,需要所需字体(ttc)文件
fontResolver.addFont(fontFile, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
renderer.setDocumentFromString(htmlTmpStr);
renderer.layout();
renderer.createPDF(outputStream);
result = outputStream.toByteArray();
outputStream.flush();
outputStream.close();
} catch (DocumentException | IOException e) {
e.printStackTrace();
log.error("pdf生成失败", e);
throw new IllegalArgumentException("pdf生成失败", e);
}
return result;
}

相关问题:

  • 关于中文不换行的问题
    在flying-saucer-pdf-itext5的9.0.6及以下版本,中文是不会换行的。
  • 关于中文字体问题
    使用itext转pdf是需要安装中文字体库的,不然中文显示不出来,在itext里面有多种引入字体的方式,其中html通过字符串转pdf的,使用以下方式引入字体库。并且在前端样式中加入font-family:SimSun;,即可显示中文。
1
2
3
4
ITextFontResolver fontResolver = renderer.getFontResolver();

// 解决中文支持问题,需要所需字体(ttc)文件
fontResolver.addFont(fontFile, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

遇到项目build打包成jar,资源路径无法找到的问题。
本地可通过new ClassPathResource(pdfFont).getPath();获取资源文件的全路径。但是jar包里是无法进入jar包内查找文件路径。

  • 关于纸张大小、换页
    指定纸张大小为a4横向排版、并且边距为0的样式:@page{size:297mm 210mm;margin:0;padding:0;margin:0}
    换页样式:
1
2
3
4
.index {
/* 后面内容归到下一页 */
page-break-after: always;
}
  • 关于引入的css样式是否需要加绝对路径问题
    很多时候,前端的样式引入是可能用相对路径的,但pdf转换的时候是必须用绝对路径的,那就要么让前端把根路径加上,又或者我们在程序里面把link标签拎出来遍历给它们加上根路径,但其实还有一种方式,也就是flying-saucer-pdf提供的一个方法renderer.setDocumentFromString(html,baseUrl)也是能达到效果,不需要我们事先加好根路径。

  • 解决图片相对路径问题
    renderer.getSharedContext().setBaseURL("http://localhost:8080");

  • 其他问题
    该转换方式的html必须是静态化的,该转换方式对html的检查非常严格,必须使用闭合的标签,否则报错。
Fork me on GitHub