原文地址:Java 渲染 docx 文件,并生成 pdf 加水印html
最近作了一个比较有意思的需求,实现的比较有意思。前端
一顿google之后发现了 StackOverflow 上的这个回答:Converting docx into pdf in java 使用以下的 jar 包:java
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。git
第二种思路,使用 LibreOffice, LibreOffice 提供了一套 api 能够提供给 java 程序调用。 因此使用 jodconverter 来调用 LibreOffice。以前网上搜到的教程早就已通过时。jodconverter 早就推出了 4.2 版本。最靠谱的文档仍是直接看官方提供的wiki。github
第一种思路,将 docx 装换为 html 的纯文本格式,再使用 Java 现有的模板引擎(freemark,velocity)渲染内容。可是 docx 文件装换为 html 仍是会有极大的格式损失。 pass。spring
第二种思路。直接操做 docx 文档在 docx 文档中直接将占位符替换为内容。这样保证了格式不会损失,可是没有现成的模板引擎能够支持 docx 的渲染。须要本身实现。apache
这个相对比较简单,直接使用 itextpdf 免费版就能解决问题。须要注意中文的问题字体,下文会逐步讲解。后端
jodconverter
已经提供了一套完整的spring-boot
解决方案,只须要在 pom.xml
中增长以下配置:api
<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>
复制代码
增长配置类:bash
@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);
}
}
}
}
}
}
复制代码
这里须要特别注意:
para.getParagraphText()
指的是获取段落,para.getRuns()
应该指的是获取词。可是问题来了,获取到的 runs 的划分是一个谜。目前我也没有找到规律,颇有可能咱们的占位符被划分到了多个run
中,若是咱们简单的针对 run
作正则表达的替换,而要先把全部的 runs
组合起来再进行正则替换。para.insertNewRun()
的时候 run
并不会保持字体样式和字体大小须要手动获取并设置。 因为以上两个蜜汁实现,因此就写了一坨蜜汁代码才能保证正则替换和格式正确。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。
这个方法使用前:
使用后:
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();
}
}
复制代码
xxx.ttf
cp xxx.ttc /usr/share/fonts
fc-cache -fv
复制代码
itextpdf
不支持汉字,须要提供额外的字体://字体路径
String fontPath = "simsun.ttf"
//设置字体
BaseFont baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
复制代码
整个需求挺有意思,可是在查询的时候发现中文文档的质量实在堪忧,要么极度过期,要么就是你们互相抄袭。 查询一个项目的技术文档,最好的路径应该以下:
项目官网 Getting Started == github demo > StackOverflow >> CSDN >> 百度知道
欢迎关注个人微信公众号