最近做了一个比较有意思的需求,实现的比较有意思。
一顿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 。
第一种思路,将 docx 装换为 html 的纯文本格式,再使用 Java 现有的模板引擎(freemark,velocity)渲染内容。但是 docx 文件装换为 html 还是会有极大的格式损失。 pass。
第二种思路。直接操作 docx 文档在 docx 文档中直接将占位符替换为内容。这样保证了格式不会损失,但是没有现成的模板引擎可以支持 docx 的渲染。需要自己实现。
这个相对比较简单,直接使用 itextpdf 免费版就能解决问题。需要注意中文的问题字体,下文会逐步讲解。
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); } } } } } }
这里需要特别注意 :
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 >>
>>
欢迎关注我的微信公众号