motto-html 是一款基于 Apache Velocity 模版引擎和 Flying Saucer 渲染器的 PDF 文档生成工具包。该工具包适用于需要从结构化数据批量生成格式化 PDF 文档的应用场景。
flowchart LR
A["模版"] -- Apache Velocity --> B["HTML"] -- Flying Saucer --> C["PDF"]
这个工具库的表达能力比 motto-pdf-itext8 更强,但是对于模版设计者来说可能会有一些不便。
- 使用 Apache Velocity 模版语法定义文档结构
- 支持 CSS 2.1 规范的样式定义
- 支持自定义字体和 CJK 字符集
- 支持嵌入本地和网络图片资源
- 支持批量文档生成
在项目的 pom.xml 文件中添加以下依赖:
<dependency>
<groupId>cc.ddrpa.motto</groupId>
<artifactId>motto-html</artifactId>
<version>${motto-html.version}</version>
</dependency>可在 Maven Central 查询最新版本号。
你可以查看本项目的单元测试了解用法,例如创建 FontBook,字体加载,图片处理,通过模板生成 PDF(包括批量生成)等。
- 准备 Velocity 模版文件
- 创建
DocumentBuilder实例 - 加载模版
- 合并数据
- 输出 PDF 文件
// 创建 DocumentBuilder 并从 classpath 加载模版
DocumentBuilder builder = new DocumentBuilder()
.loadTemplate("record-template.html");
// 添加数据,这个步骤可以重复多次
builder.merge(Map.of(
"name", faker.name().fullName(),
"idCard", faker.idNumber().invalidSvSeSsn(),
"category", "吃瓜群众",
"records", List.of(
new CourseRecord(1, "课程名称1", "培训策略1", 11, 100),
new CourseRecord(2, "课程名称2", "培训策略2", 22, 98),
new CourseRecord(3, "课程名称3", "培训策略3", 33, 95)
)
));
// 向给定的输出流保存 PDF 文件
try (FileOutputStream fos = new FileOutputStream("output.pdf")) {
builder.save(fos);
}模版文件使用 Apache Velocity 语法。有关 Velocity 模版语言的详细信息,请参阅 Apache Velocity 用户指南。
模版样式必须遵循 CSS 2.1 规范,Flying Saucer 渲染器能够支持部分 CSS3 特性,包括页面控制属性,除此之外可能需要自行测试。
@page {
/* A4 大小,竖版 */
size: A4 portrait;
/* 或者横版 size: A4 landscape;*/
/* 用 margin 指定页边距也是支持的 */
}你可以参考 How do you control page size?,对手动分页等内容也做了详细的说明。
- 模板样式应当遵循 Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification;
- 尽管
<img>这类标签支持自闭合,请使用<img></img>; - 使用
pt设置尺寸,特别是在使用了EmbeddedImage#setDevicePixelRatio处理图片的情况下; - 设置字体族(
font-family)时,首选项必须是STSong/STSongStd或其他预先部署到服务器环境并由程序显式读取的字体; - 观察到
E > F这种 Child selectors 在某些情况下似乎没有正确的应用font-family属性,因此如果输出文件中没有出现字符,请在 DOM 元素上通过内联样式设置font-family; - 请把
<style>标签放在<head>里,不要放在<body>之中或之后; - 请使用 Ctrl + P 或 Cmd + P 预览效果,而不是 DevTools;
- 不要尝试在模版内使用 JavaScript;
如果对字体没有要求,可将中文文本的 font-family 属性设置为以下值之一:
STSong-Light-H是一种衬线字体STSongStd-Light-H是一种无衬线字体
使用 DocumentBuilder.addFont() 静态方法注册外部字体文件:
DocumentBuilder.addFont("/path/to/NotoSansSC-Regular.ttf");注意: 字体文件通常较大,不建议打包至 JAR 文件中。建议将字体文件部署至服务器文件系统。
自 1.2.2 版本起,可使用以下方法加载系统预装的字体:
List<String> loadedFonts = DocumentBuilder.loadPreinstalledFontsAsCJKFont();该方法返回成功加载的字体路径列表。这些字体会按照 CJK 字体来加载,因此可能产生意料之外的问题,可以执行 cc.ddrpa.motto.html.LoadPreInstalledFontsTests 预览效果。
List<String> fontFamilies = DocumentBuilder.listFontFamily();商业项目请确保拥有合法的字体授权,本项目推荐使用三种开源字体,见 font-seems-okay/ 目录:
- Noto Sans CJK - Google 出品的无衬线字体,可用于黑体
- Noto Serif CJK - Google 出品的衬线字体,可用于宋体
- 寒蝉正楷体 - 可用于楷体
网络图片可直接在模版中引用:
<img src="https://example.com/image.jpg"></img>1.2.3 版本增加了通过 ResourcesUserAgent 配置访问网络资源的超时时间:
ResourcesUserAgent userAgent = new ResourcesUserAgent(outputDevice, dotsPerPixel)
.setHttpConnectTimeout(5, TimeUnit.SECONDS)
.setHttpReadTimeout(10, TimeUnit.SECONDS);访问资源失败时,该图片将被跳过。
如果资源文件打包在 classpath 中,可使用 resources:// 协议引用:
<img src="resources://images/logo.png"></img>上述路径对应 src/main/resources/images/logo.png。
对于需要压缩或调整尺寸的图片,使用 cc.ddrpa.motto.html.embedded.EmbeddedImage 类:
EmbeddedImage compressed = EmbeddedImage.newInstance(fis)
.setDotsPerPoint(dotsPerPoint)
.setDotsPerPixel(dotsPerPixel)
.setDevicePixelRatio(devicePixelRatio)
.scaleWithPoint(228, 128);
builder.merge("image-preview", compressed);在模版中引用:
<img src="$avatar" style="width: 200pt; height: 100pt;"></img>如果修改了
devicePixelRatio,必须在模版中使用pt单位限制图片尺寸。
使用 reset() 方法重置 builder 状态,实现批量文档生成:
DocumentBuilder builder = new DocumentBuilder()
.loadTemplate("certificate.html");
for (Person person : personList) {
builder.merge(Map.of("name", person.getName(), "date", person.getDate()));
try (FileOutputStream fos = new FileOutputStream(person.getId() + ".pdf")) {
builder.save(fos);
}
builder.reset(); // 重置状态,准备下一份文档
}| 方法 | 说明 |
|---|---|
loadTemplate(String) |
从 classpath 加载模版文件 |
loadTemplateFromStream(InputStream) |
从输入流加载模版 |
loadTemplateFromPlainText(String) |
从字符串加载模版 |
merge(Map<String, Object>) |
合并数据至模版上下文 |
merge(String, Object) |
合并单个键值对 |
save(OutputStream) |
输出 PDF 至指定流 |
reset() |
重置 builder 状态 |
| 方法 | 说明 |
|---|---|
newInstance(InputStream) |
从输入流创建实例 |
newInstance(BufferedImage) |
从 BufferedImage 创建实例 |
newInstance(byte[]) |
从字节数组创建实例 |
setDotsPerPoint(float) |
设置点分辨率 |
setDotsPerPixel(int) |
设置像素分辨率 |
setDevicePixelRatio(int) |
设置设备像素比 |
scaleWithPoint(int, int) |
按点单位缩放 |
scaleWithMillimetre(double, double) |
按毫米单位缩放 |
toDataURL() |
转换为 Data URL |
