使用Apache POI/Docx4j将Docx转PDF遇排版问题求助
Docx转PDF:Apache POI/Docx4j排版问题解决方案及工具选择
问题概述
我尝试用Apache POI或Docx4j(基于Apache FOP)将Docx文档转PDF,但遇到两个排版问题:
- Apache POI转换时,原文档无换行的位置会多出不必要的换行
- Docx4j转换时,表格标题和表格会被强制移到下一页
作为新手,想知道哪个工具更适合我的场景,以及如何解决这些问题。
工具对比与选择建议
| 工具 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Apache POI | 轻量、集成简单,API学习成本低 | PDF转换模块对复杂排版支持有限,细节处理粗糙 | 简单文档批量转换 |
| Docx4j | 对Word原生格式支持全面,排版还原度高 | 配置稍复杂,资源占用略高 | 含复杂表格、自定义排版的文档 |
如果你的文档以简单文本为主,选Apache POI足够;如果涉及复杂排版(如表格、多级标题、分页规则),优先选Docx4j,它的排版还原效果更贴近原Word文档。
一、解决Apache POI的多余换行问题
问题原因
POI的PDF转换器对段落的换行规则、字符间距处理不够精准,尤其是文档包含自定义段落格式或特殊字体时,容易出现多余换行。
解决方案
- 升级依赖版本:确保使用最新稳定版的POI(如5.2.5+)和对应的
fr.opensagres.xdocreport.converter(2.0.2+),新版本修复了不少排版bug。 - 预处理文档段落:遍历文档段落,移除不必要的换行标记,统一段落格式。
- 配置字体映射:避免因字体缺失导致的排版错乱。
优化后的代码
try { oLog.error("function POI begin"); String wordDocFile = "C:\\PRPCPersonalEdition\\tomcat\\TestDocument.docx"; String pdfDocFile = "C:\\PRPCPersonalEdition\\tomcat\\TestDocument.pdf"; oLog.error("WordDocFile is " + wordDocFile); oLog.error("pdfDocFile is " + pdfDocFile); InputStream iStream = new FileInputStream(wordDocFile); OutputStream os = new FileOutputStream(pdfDocFile); XWPFDocument document = new XWPFDocument(iStream); oLog.error("Loaded XWPFDocument"); // 预处理段落,去除多余换行 for (XWPFParagraph para : document.getParagraphs()) { String cleanText = para.getText().replaceAll("\\n+", " ").trim(); // 清空原有run,重新设置文本 while (para.getRuns().size() > 0) { para.removeRun(0); } para.createRun().setText(cleanText); } // 配置字体映射,替换为本地存在的字体 PdfOptions options = PdfOptions.create() .fontProvider((fontName, encoding, size, style, color) -> { try { // 替换为你的本地字体路径,比如宋体 return BaseFont.createFont("C:\\Windows\\Fonts\\simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } catch (Exception e) { // fallback到默认字体 return BaseFont.createFont(); } }); PdfConverter.getInstance().convert(document, os, options); iStream.close(); os.close(); return "Success"; } catch(Exception e) { e.printStackTrace(); oLog.error("print exception " + e); return "FAIL"; }
二、解决Docx4j的表格标题与表格分页问题
问题原因
Docx4j默认的分页策略会将无法在当前页完整显示的元素整体移到下一页,而表格标题作为独立段落,未和表格设置“保持在一起”的属性,导致两者被拆分到不同页面。
解决方案
- 设置段落的“keep with next”属性:让表格标题和后续表格保持在同一页。
- 设置表格的“keep lines”属性:避免表格自身被分页拆分。
- 调整页面边距:增加当前页的可容纳空间。
优化后的代码
try { String wordDocFile = "C:\\PRPCPersonalEdition\\tomcat\\TestDocument.docx"; String pdfDocFile = "C:\\PRPCPersonalEdition\\tomcat\\TestDocument.pdf"; InputStream iStream = new FileInputStream(wordDocFile); WordprocessingMLPackage pack = WordprocessingMLPackage.load(iStream); MainDocumentPart docPart = pack.getMainDocumentPart(); ObjectFactory factory = pack.getObjectFactory(); // 遍历文档内容,为表格标题设置"keep with next",为表格设置不跨页 List<Object> content = docPart.getContent(); for (int i = 0; i < content.size(); i++) { Object obj = content.get(i); if (obj instanceof P) { P para = (P) obj; // 根据实际文档的标题样式判断,比如样式名为"TableTitle"或"Heading2" if (para.getPPr() != null && para.getPPr().getStyle() != null) { String styleId = para.getPPr().getStyle().getVal(); if ("TableTitle".equals(styleId)) { // 启用"与下一段保持在一起" if (para.getPPr().getKeepNext() == null) { para.getPPr().setKeepNext(factory.createKeepNext()); } para.getPPr().getKeepNext().setVal(STOnOff.ON); } } } else if (obj instanceof Tbl) { Tbl table = (Tbl) obj; // 设置表格不跨页拆分 if (table.getTblPr() == null) { table.setTblPr(factory.createTblPr()); } if (table.getTblPr().getKeepLines() == null) { table.getTblPr().setKeepLines(factory.createKeepLines()); } table.getTblPr().getKeepLines().setVal(STOnOff.ON); } } // 配置PDF转换的页面参数,调整边距 PdfConversionOptions options = PdfConversionOptions.getDefaults(); options.setMarginTop(BigInteger.valueOf(720)); // 上边距1英寸(720 twip) options.setMarginBottom(BigInteger.valueOf(720)); options.setMarginLeft(BigInteger.valueOf(720)); options.setMarginRight(BigInteger.valueOf(720)); OutputStream os = new FileOutputStream(pdfDocFile); Docx4J.toPDF(pack, os, options); iStream.close(); os.close(); return "Success"; } catch(Exception e) { e.printStackTrace(); oLog.error("print exception " + e); return "FAIL"; }
三、修复你提供的iText代码问题
你给出的iText代码存在osCurrentStream未初始化就使用的空指针问题,以下是优化后的版本,直接用字节流处理转换后的PDF:
try { oLog.error("function PDF iText begin"); String wordDocFile = "C:\\PRPCPersonalEdition\\tomcat\\DraftMin.docx"; String pdfDocFile = "C:\\PRPCPersonalEdition\\tomcat\\DraftMinItext12.pdf"; InputStream iStream = new FileInputStream(wordDocFile); oLog.error("Read Input file " + iStream); XWPFDocument document = new XWPFDocument(iStream); PdfOptions options = PdfOptions.create(); // 先将Docx转成PDF字节流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfConverter.getInstance().convert(document, baos, options); // 使用iText处理PDF文档 com.itextpdf.text.Document finalDocument = new com.itextpdf.text.Document(); finalDocument.addAuthor("Your Corporation"); finalDocument.addCreationDate(); finalDocument.addProducer(); finalDocument.addCreator("System"); finalDocument.addTitle("Your Corporation"); finalDocument.setPageSize(com.itextpdf.text.PageSize.A4); OutputStream os = new FileOutputStream(pdfDocFile); com.itextpdf.text.pdf.PdfCopy copy1 = new com.itextpdf.text.pdf.PdfCopy(finalDocument, os); finalDocument.open(); // 读取转换后的PDF字节流 com.itextpdf.text.pdf.PdfReader.unethicalreading = true; com.itextpdf.text.pdf.PdfReader readingpdf = new com.itextpdf.text.pdf.PdfReader(baos.toByteArray()); int pageCount = readingpdf.getNumberOfPages(); // 遍历所有页面,注意原代码漏掉最后一页(i <= pageCount) for (int i = 1; i <= pageCount; i++) { oLog.error("Entered in loop " + i); copy1.addPage(copy1.getImportedPage(readingpdf, i)); oLog.error("Read Page n : " + i); } // 关闭资源 finalDocument.close(); readingpdf.close(); baos.close(); iStream.close(); os.close(); return "Success"; } catch(Exception e) { e.printStackTrace(); oLog.error("print exception " + e); return "FAIL"; }
内容的提问来源于stack exchange,提问作者Sonakshi
相关产品推荐
相关产品推荐

