前言

最近笔者在公司内部分享了一下个人处理 Word 模板的技巧,公司内部对文章质量比较满意,故搬运到个人的博客上,希望能帮助到有缘人。

1、流程梳理

系统中的 Output Service 专注于提供 Output 的配置能力和打印能力,每个 Word 模板的打印流程可以简化为以下几步

flowchart TD
    Start([开始]) --> loadTemplate[读取 Word 模板]
    loadTemplate --> loadRenderParams[读取渲染参数]
    loadRenderParams --> poiTlRender[POI-TL 根据模板和参数渲染 Word]
    poiTlRender --> asposeConvertPDF[Aspose 将渲染好的 Word 转换为 PDF]
    asposeConvertPDF --> End([结束])

POI-TL 免费且开源的,Aspose 付费且闭源。

注:Output Service 的渲染参数依赖于外界传入,系统中将所有的渲染参数都格式化成了 Map,仅保留其 Key-Value 的性质,丧失了相关的对象信息。

2、Office / WPS

国内常用办公软件可以分为两类:OfficeWPS,同一个文档在不同的办公软件中打开有可能会呈现出不一样的展示效果,而 POI-TLAspose 都遵循所见即所得的设计理念,那么他们到底是以 Office 的展示效果为准,还是以 WPS 的展示效果为准?

分别用 OfficeWPS 打开同一个 Word 文档:

image

代码渲染好的结果:

image

可以很明显看到是以 Office 的展示效果为准,因此在制作 Word 模板时为了所见即所得请务必使用 Office

3、动态渲染技巧

动态渲染中最常出现的操作对象就是表格,如果将一个列表中的数据转化到表格模板中去?POI-TL 提供了一个非常好用的插件:表格行循环表格列循环,POL-TL 还支持自定义插件。除了插件还有一个很好用的功能:区块对,它能够根据一定的条件展示或者是不展示块内容,也具有循环渲染的能力。

这里举一个复杂的示例:以下的表格中代表的是附加险,需要达到的效果是客户选了哪些附加险就展示哪些,红色部分为产品配置中对应附加险配置的额度。

image

分析过程:

  1. 表格行循环插件:棘手的点在于每一个单元格中文本的格式是多样性的,该模板承载了英语和马来语,马来语被设置为了斜体,这种样式的问题虽然也能够通过插件默认解决,但是默认插件之间无法嵌套使用。

    虽然 POL-TL 提供了自定义渲染样式的默认插件,但该插件要求要使用指定的对象来进行包装,而在中 1、流程梳理 提到过数据的对象信息会丢失,从而导致即使引用了该插件也不能直接达到想要的效果。

  2. 自定义插件:整个表格的渲染完全由代码控制,该方式既要关注取值又要关注样式,过程坑定是费时又费力,只能是走投无路的下下策。

  3. 区块对:将可能出现的表格都列举出来,通过区块对来控制究竟该显示哪个表格,但这样相当于是暴力枚举,如果情况过多则需要列举所有的可能性,也不能说的上是一种好方法。

    image

    Word 的表格有一个很重要的特性:两个格式一致但却被拆开的表格,如果中间的空隙能被消除则能合并成一个表格。借助这个特性和区块对嵌套就能比较完美地实现该需求:

    image

    按照个人的开发经验来说,掌握上述的功能点能解决绝大多数的渲染需求,小部分比较难的渲染需求就得老老实实自定义插件实现。

4、模板语言

某些语言打印出来的 PDF 与 Word 模板中的语义可能会有较大的差距,目前已知有问题的语言:缅甸语和阿拉伯语。

image

问题的根本原因是 Aspose Word For Java 存在一定的缺陷,由于公司是改框架的付费用户,在反馈给官方之后获取到了一个拓展 Jar 包并在官方的指引下解决了问题。

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
import com.aspose.words.Document
import com.aspose.words.License
import com.aspose.words.SaveFormat
import com.aspose.words.shaping.harfbuzz.HarfBuzzTextShaperFactory
import groovy.util.logging.Slf4j
import org.springframework.core.io.Resource

import org.springframework.core.io.support.PathMatchingResourcePatternResolver

@Slf4j
class Word2PdfUtils {

static void convertByAsposeWords(InputStream inputStream, OutputStream pdf) {
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver()
Resource resource = resolver.getResource("classpath:license.xml")
if (resource.exists()) {
License license = new License()
license.setLicense(resource.getInputStream())
} else {
log.warn("Failed to load license.xml")
}
Document doc = new Document(inputStream)
doc.getLayoutOptions().setTextShaperFactory(HarfBuzzTextShaperFactory.getInstance()) // 这行是关键
doc.save(pdf, SaveFormat.PDF)
}

}

引入该 Jar 包之后发布之前一定要做的一件事情:确保服务器对 java.io.tmpdir 文件夹有读写执行权限

为什么需要那么高的权限?是因为官方给出的拓展 Jar 里面实际上主要包含的是动态链接库和 native 方法,运行含有动态链接库的 Jar 包时,进程会尝试往 java.io.tmpdir 文件夹里放入动态链接库文件以便后续调用。

修改之后也只能保证”看起来”和打印出来没问题,如果直接在 PDF 中复制有问题的语句,粘贴出来依然可能有问题。PDF 文件本身也不支持直接的修改,故没有进一步的修复。