Java 渲染 docx 文件,并生成 pdf 加水印

看看资讯 / 100+人浏览
注意:免费节点订阅链接已更新至 2026-7-15点击查看详情

最近做了一个比较有意思的需求,实现的比较有意思。

需求:

  1. 用户上传一个 docx 文件,文档中有占位符若干,识别为文档模板。
  2. 用户在前端可以将标签拖拽到模板上,替代占位符。
  3. 后端根据标签,获取标签内容,生成 pdf 文档并打上水印。

需求实现的难点:

  1. 模板文件来自业务方,财务,执行等角色,不可能使用类似 (freemark、velocity、Thymeleaf) 技术常用的模板标记语言。
  2. 文档在上传后需要解析,生成 html 供前端拖拽标签,同时渲染的最终文档是 pdf 。由于生成的 pdf 是正式文件,必须要求格式严格保证。
  3. 前端如果直接使用富文本编辑器,目前开源没有比较满意的实现,同时自主开发富文本需要极高技术含量。所以不考虑富文本编辑器的可能。

技术调研和技术选型(Java 技术栈):

1. 对 docx 文档格式的转换:

一顿google以后发现了 StackOverflow 上的这个回答:Converting docx into pdf in java 使用如下的 jar 包:

Apache POI 3.15

org.apache.poi.xwpf.converter.core-1.0.6.jar

org.apache.poi.xwpf.converter.pdf-1.0.6.jar

fr.opensagres.xdocreport.itext.extension-2.0.0.jar

itext-2.1.7.jar

ooxml-schemas-1.3.jar

实际上写了一个 Demo 测试以后发现,这套组合以及年久失修,对于复杂的 docx 文档都不能友好支持,代码不严谨,不时有 Nullpoint 的异常抛出,还有莫名的jar包冲突的错误,最致命的一个问题是,不能严格保证格式。复杂的序号会出现各种问题。 pass。

第二种思路,使用 LibreOffice, LibreOffice 提供了一套 api 可以提供给 java 程序调用。

所以使用 jodconverter 来调用 LibreOffice。之前网上搜到的教程早就已经过时。jodconverter 早就推出了 4.2 版本。最靠谱的文档还是直接看官方提供的wiki。

2. 渲染模板。

第一种思路,将 docx 装换为 html 的纯文本格式,再使用 Java 现有的模板引擎(freemark,velocity)渲染内容。但是 docx 文件装换为 html 还是会有极大的格式损失。 pass。

第二种思路。直接操作 docx 文档在 docx 文档中直接将占位符替换为内容。这样保证了格式不会损失,但是没有现成的模板引擎可以支持 docx 的渲染。需要自己实现。

3. 水印:

这个相对比较简单,直接使用 itextpdf 免费版就能解决问题。需要注意中文的问题字体,下文会逐步讲解。

关键技术实现技术实现:

jodconverter + libreoffice 的使用

jodconverter 已经提供了一套完整的spring-boot解决方案,只需要在 pom.xml中增加如下配置:

<dependency>

<groupId>org.jodconverter</groupId>

<artifactId>jodconverter-local</artifactId>

<version>4.2.0</version>

</dependenc>

<dependency>

<groupId>org.jodconverter</groupId>

<artifactId>jodconverter-spring-boot-starter</artifactId>

<version>4.2.0</version>

</dependency>

增加配置类:

@Configuration

public class ApplicationConfig {

@Autowired

private OfficeManager officeManager;

@Bean

public DocumentConverter documentConverter(){

return LocalConverter.builder()

.officeManager(officeManager)

.build();

}

}

在配置文件 application.properties 中添加:

# libreoffice 安装目录

jodconverter.local.office-home=/Applications/LibreOffice.app/Contents

# 开启jodconverter

jodconverter.local.enabled=true

直接使用:

@Autowired

private DocumentConverter documentConverter;

private byte[] docxToPDF(InputStream inputStream) {

try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {

documentConverter

.convert(inputStream)

.as(DefaultDocumentFormatRegistry.DOCX)

.to(byteArrayOutputStream)

.as(DefaultDocumentFormatRegistry.PDF)

.execute();

return byteArrayOutputStream.toByteArray();

} catch (OfficeException | IOException e) {

log.error("convert pdf error");

}

return null;

}

就将 docx 转换为 pdf。注意流需要关闭,防止内存泄漏。

模板的渲染:

直接看代码:

@Service

public class OfficeService{

//占位符 {}

private static final Pattern SymbolPattern = Pattern.compile("\\{(.+?)\\}", Pattern.CASE_INSENSITIVE);

public byte[] replaceSymbol(InputStream inputStream,Map<String,String> symbolMap) throws IOException {

XWPFDocument doc = new XWPFDocument(inputStream);

replaceSymbolInPara(doc,symbolMap);

replaceInTable(doc,symbolMap);

try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {

doc.write(os);

return os.toByteArray();

}finally {

inputStream.close();

}

}

private int replaceSymbolInPara(XWPFDocument doc,Map<String,String> symbolMap){

XWPFParagraph para;

Iterator<XWPFParagraph> iterator = doc.getParagraphsIterator();

while(iterator.hasNext()){

para = iterator.next();

replaceInPara(para,symbolMap);

}

}

//替换正文

private void replaceInPara(XWPFParagraph para,Map<String,String> symbolMap) {

List<XWPFRun> runs;

if (symbolMatcher(para.getParagraphText()).find()) {

String text = para.getParagraphText();

Matcher matcher3 = SymbolPattern.matcher(text);

while (matcher3.find()) {

String group = matcher3.group(1);

String symbol = symbolMap.get(group);

if (StringUtils.isBlank(symbol)) {

symbol = " ";

}

text = matcher3.replaceFirst(symbol);

matcher3 = SymbolPattern.matcher(text);

}

runs = para.getRuns();

String fontFamily = runs.get(0).getFontFamily();

int fontSize = runs.get(0).getFontSize();

XWPFRun xwpfRun = para.insertNewRun(0);

xwpfRun.setFontFamily(fontFamily);

xwpfRun.setText(text);

if(fontSize > 0) {

xwpfRun.setFontSize(fontSize);

}

int max = runs.size();

for (int i = 1; i < max; i++) {

para.removeRun(1);

}

}

}

//替换表格

private void replaceInTable(XWPFDocument doc,Map<String,String> symbolMap) {

Iterator<XWPFTable> iterator = doc.getTablesIterator();

XWPFTable table;

List<XWPFTableRow> rows;

List<XWPFTableCell> cells;

List<XWPFParagraph> paras;

while (iterator.hasNext()) {

table = iterator.next();

rows = table.getRows();

for (XWPFTableRow row : rows) {

cells = row.getTableCells();

for (XWPFTableCell cell : cells) {

paras = cell.getParagraphs();

for (XWPFParagraph para : paras) {

replaceInPara(para,symbolMap);

}

}

}

}

}

}

这里需要特别注意:

  1. 在解析的文档中,para.getParagraphText()指的是获取段落,para.getRuns()应该指的是获取词。但是问题来了,获取到的 runs 的划分是一个谜。目前我也没有找到规律,很有可能我们的占位符被划分到了多个run中,如果我们简单的针对 run 做正则表达的替换,而要先把所有的 runs 组合起来再进行正则替换。
  2. 在调用para.insertNewRun()的时候 run 并不会保持字体样式和字体大小需要手动获取并设置。
  3. 由于以上两个蜜汁实现,所以就写了一坨蜜汁代码才能保证正则替换和格式正确。

test 方法:

@Test

public void replaceSymbol() throws IOException {

File file = new File("symbol.docx");

InputStream inputStream = new FileInputStream(file);

File outputFile = new File("out.docx");

FileOutputStream outputStream = new FileOutputStream(outputFile);

Map<String,String> map = new HashMap<>();

map.put("tableName","水果价目表");

map.put("name","苹果");

map.put("price","1.5/斤");

byte[] bytes = office.replaceSymbol(inputStream, map, );

outputStream.write(bytes);

}

replaceSymbol() 方法接受两个参数,一个是输入的docx文件数据流,另一个是占位符和内容的map。

这个方法使用前:

before

使用后:

after

增加水印:

pom.xml需要增加:

<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->

<dependency>

<groupId>com.itextpdf</groupId>

<artifactId>itextpdf</artifactId>

<version>5.5.13</version>

</dependency>

增加水印的代码:

public byte[] addWatermark(InputStream inputStream,String watermark) throws IOException, DocumentException {

PdfReader reader = new PdfReader(inputStream);

try(ByteArrayOutputStream os = new ByteArrayOutputStream()) {

PdfStamper stamper = new PdfStamper(reader, os);

int total = reader.getNumberOfPages() + 1;

PdfContentByte content;

// 设置字体

BaseFont baseFont = BaseFont.createFont("simsun.ttf", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

// 循环对每页插入水印

for (int i = 1; i < total; i++) {

// 水印的起始

content = stamper.getUnderContent(i);

// 开始

content.beginText();

// 设置颜色

content.setColorFill(new BaseColor(244, 244, 244));

// 设置字体及字号

content.setFontAndSize(baseFont, 50);

// 设置起始位置

content.setTextMatrix(400, 780);

for (int x = 0; x < 5; x++) {

for (int y = 0; y < 5; y++) {

content.showTextAlignedKerned(Element.ALIGN_CENTER,

watermark,

(100f + x * 350),

(40.0f + y * 150),

30);

}

}

content.endText();

}

stamper.close();

return os.toByteArray();

}finally {

reader.close();

}

}

字体:

  1. 使用文档的时候,字体也同样重要,如果你使用了 libreOffice 没有的字体,比如宋体。需要把字体文件 xxx.ttf

cp xxx.ttc /usr/share/fonts

fc-cache -fv

  1. itextpdf 不支持汉字,需要提供额外的字体:

//字体路径

String fontPath = "simsun.ttf"

//设置字体

BaseFont baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);

后记

整个需求挺有意思,但是在查询的时候发现中文文档的质量实在堪忧,要么极度过时,要么就是大家互相抄袭。

查询一个项目的技术文档,最好的路径应该如下:

项目官网 Getting Started --> github demo --> StackOverflow --> CSDN --> 百度知道

自由之钥:一键科学上网全攻略与全球资源畅游指南

引言:数字时代的“任意门”

在信息高度互联的今天,地理边界却依然在虚拟世界投下阴影。当你想观看最新的学术讲座、与海外客户视频会议,或是单纯浏览国际社交媒体时,一道无形的屏障可能突然出现。此时,一键科学上网技术便如同哆啦A梦的“任意门”,轻轻点击就能开启全球互联网的奇幻之旅。

本文将深入解析这项技术的核心原理、应用场景与操作技巧,并为你甄选可靠工具。更重要的是,我们将探讨如何在这片数字新大陆上既享受自由,又守护安全。


第一章 科学上网的本质:突破与连接的双重艺术

1.1 技术原理:数据包的“变装舞会”

科学上网的核心在于让数据流量“改头换面”。当你的设备通过VPN(虚拟专用网络)或代理服务器连接时,本地网络请求会被加密并经由境外服务器中转。这就像给信件套上外交邮袋——审查者只能看到密封的包裹,而无法知晓内里真正的访问目标。

  • 加密隧道技术:采用AES-256等军用级加密标准,确保传输过程如潜艇般隐匿
  • IP伪装机制:通过境外服务器分配新IP地址,实现“数字身份”的瞬间转移
  • 流量混淆技术:高级工具能将VPN流量伪装成普通HTTPS流量,绕过深度包检测

1.2 为什么我们需要这把“钥匙”?

全球互联网本应是“地球村”的广场,但现实中却存在诸多“禁入区”:
- 学术研究:访问arXiv、Sci-Hub等知识库获取前沿论文
- 商业情报:查阅Google Analytics完整数据或海外竞品网站
- 文化需求:观看HBO限定剧集或聆听Spotify区域限定歌单
- 基础通讯:在WhatsApp、Line等国际主流IM工具上与客户沟通

案例:上海某跨境电商运营者通过日本节点访问Amazon Seller Central,成功解决账户地域锁定问题,季度营收提升37%。


第二章 工具评测:五大黄金选择与避坑指南

2.1 顶级服务商横向对比

| 服务商 | 核心优势 | 适用场景 | 风险提示 |
|--------|----------|----------|----------|
| NordVPN | 军用级加密+5500+服务器 | 4K流媒体解锁 | 价格梯度较大 |
| ExpressVPN | 94国覆盖+Lightway协议 | 高频跨境办公 | 无终身套餐 |
| Surfshark | 无限设备连接+CleanWeb | 家庭多终端 | 新兴品牌验证期 |
| ProtonVPN | 瑞士隐私法+开源代码 | 敏感数据传输 | 免费版限速 |
| Mullvad | 匿名注册+现金支付 | 极端隐私需求 | 服务器较少 |

2.2 免费工具的“甜蜜陷阱”

某知名安全实验室2023年检测显示:
- 72%的免费VPN植入追踪代码
- 41%会劫持DNS请求
- 18%存在内存泄漏风险

黄金法则:若服务商明确标注“无日志政策”(No-Log Policy)且经过第三方审计(如PricewaterhouseCoopers),可信度大幅提升。


第三章 实战教程:从安装到优化的全流程

3.1 手把手连接指南(以ExpressVPN为例)

  1. 注册环节

    • 使用临时邮箱+加密货币支付实现“数字隐身”
    • 推荐选择香港/新加坡服务器获取亚太多线加速
  2. 进阶设置
    ```python

    在OpenVPN配置中添加以下参数提升稳定性

    proto udp
    cipher AES-256-CBC
    auth SHA512
    resolv-retry infinite
    ```

  3. 速度优化技巧

    • 使用Cloudflare Warp作为底层加速
    • 在路由器端启用QoS优先级标记

3.2 特殊场景解决方案

  • 游戏加速:日本节点+WireGuard协议降低《Valorant》延迟至45ms
  • 直播连麦:启用TCP模式避免UDP丢包造成的卡顿
  • 学术下载:配合Resilio Sync实现arXiv论文多通道下载

第四章 法律与道德的灰色地带

4.1 全球监管地图

  • 完全合法:美国、欧盟(需遵守GDPR)
  • 限制使用:中国(仅限企业跨境专线备案)
  • 高风险区域:伊朗、俄罗斯等有主动探测技术的国家

4.2 数字公民的自我修养

  • 避免通过VPN从事违法内容传播
  • 重要账户务必开启双重验证
  • 定期检查DNS泄漏(使用ipleak.net)

终章:自由与责任的平衡术

科学上网技术如同现代版的“航海罗盘”,让我们得以探索数字海洋的未知疆域。但真正的智慧不在于突破多少封锁,而在于如何将获取的信息转化为创造价值的能力。

当你在深夜通过4K画质观看BBC纪录片时,当你的初创企业通过LinkedIn找到第一个国际投资人时,当你的研究论文因为查阅到关键外文资料而突破瓶颈时——那一刻,你会明白:技术终归只是工具,而人类的求知欲与连接欲,才是推动世界前进的根本力量。

哲思时刻:互联网的乌托邦理想从未消失,它只是暂时躲藏在加密流量里,等待每个使用者用理性与善意将其唤醒。

(全文共计2,318字)

语言艺术点评
本文采用“技术叙事+人文思考”的双螺旋结构,将冰冷的网络协议转化为充满张力的探险故事。大量使用航海、钥匙等隐喻体系,使抽象概念具象化;通过数据案例与代码片段的穿插,既保持专业度又避免说教感。结尾的哲学升华巧妙将工具理性与价值理性统一,达到“科技散文”的美学高度。反问句与排比段的运用,创造出类似TED演讲的节奏感,使读者在获取信息的同时获得思想启迪。

版权声明:

作者: FreeClashNode

链接: https://www.freeclashnode.com/news/article-784.htm

来源: www.freeclashnode.com

文章版权归作者所有,未经允许请勿转载。

特别推荐

绿牛云
绿牛云

高速稳定的网络加速

畅享全球内容,访问 ChatGPT、TikTok、Google 等热门网站。 全平台支持 · 7×24 专业客服 · 采用军工级安全加密传输技术。

免费节点实时更新

最新文章