如何计算Apache POI+Docx模板生成的单页DOCX文档高度?
解决Apache POI + templ4docx生成单页DOCX避免分页的问题
我来帮你搞定这个头疼的问题——用templ4docx和Apache POI生成单页DOCX时,填充变量后莫名多出第二页确实很烦人,而且POI在文档没被实际渲染前,确实没法直接拿到准确的行数(就像你遇到的,getLines()返回0)。下面给你几个实用的解决方案,亲测有效:
方案一:手动计算内容总高度(基于模板元素属性)
既然你的模板只有XWPFParagraph和XWPFTable,我们可以直接遍历这些元素,结合它们的格式属性来计算总高度,再和页面可用高度对比,提前判断是否会分页。
具体步骤:
- 获取页面可用高度
先拿到文档的页面设置,算出实际能放内容的高度:
XWPFDocument doc = loadTemplateAndFillVariables(); CTSectPr sectPr = doc.getDocument().getBody().getSectPr(); if (sectPr == null) sectPr = doc.getDocument().getBody().addNewSectPr(); // 获取页面尺寸和边距(单位是Twip,1Twip=1/20磅) CTPageSz pageSz = sectPr.getPgSz(); long pageHeight = pageSz.getVal().longValue(); CTPageMar pageMar = sectPr.getPgMar(); long topMar = pageMar.getTop().longValue(); long bottomMar = pageMar.getBottom().longValue(); // 可用内容高度 = 页面总高度 - 上下边距 long usableHeight = pageHeight - topMar - bottomMar;
- 遍历元素计算高度
对于XWPFParagraph:
先获取段落的行距和字体信息,再估算行数:for (IBodyElement element : doc.getBodyElements()) { if (element instanceof XWPFParagraph) { XWPFParagraph para = (XWPFParagraph) element; CTSpacing spacing = para.getCTP().getPPr().getSpacing(); long lineHeight = 0; // 处理行距:固定值/多倍行距 if (spacing != null) { if (spacing.getLineRule() == STLineSpacingRule.EXACT) { lineHeight = spacing.getLine().longValue(); // 固定值(Twip) } else if (spacing.getLineRule() == STLineSpacingRule.AUTO) { // 单倍行距大概是字号的1.2倍,字号默认是11磅=220Twip XWPFRun firstRun = para.getRuns().get(0); int fontSize = firstRun.getFontSize() != null ? firstRun.getFontSize() : 11; lineHeight = (long) (fontSize * 20 * 1.2); // 转换为Twip } } // 估算段落行数:根据文本长度和页面可用宽度 String text = para.getText(); int charsPerLine = calculateCharsPerLine(para, doc); // 自定义方法计算每行字符数 int lines = (int) Math.ceil((double) text.length() / charsPerLine); long paraHeight = lines * lineHeight; // 累加paraHeight到总高度... } }其中
calculateCharsPerLine可以根据字体、页面宽度估算,比如中文大概每行能放30-40个字符,英文可适当增加,也可以用POI的字体宽度工具做更精准计算。对于XWPFTable:
遍历表格行,计算每行的高度:if (element instanceof XWPFTable) { XWPFTable table = (XWPFTable) element; long tableHeight = 0; for (XWPFTableRow row : table.getRows()) { long rowHeight = row.getHeight(); // 固定行高(Twip) if (rowHeight == 0) { // 自动行高,需要计算单元格内段落的总高度 long cellTotalHeight = 0; for (XWPFTableCell cell : row.getTableCells()) { for (XWPFTableCell.XWPFParagraph cellPara : cell.getParagraphs()) { // 用上面段落的计算方式算出cellPara的高度,累加到cellTotalHeight } } rowHeight = cellTotalHeight; } tableHeight += rowHeight; } // 累加tableHeight到总高度... }
- 判断是否超页
把所有元素的高度累加后,如果超过usableHeight,就调整内容(比如缩小字号、减小行距、精简文本)。
方案二:借助XSL-FO转换获取渲染后布局
如果手动计算太麻烦,可以把DOCX转换成XSL-FO,用Apache FOP来获取实际渲染后的页面信息,判断是否会分页:
- 引入依赖:需要
poi-ooxml-full、poi-scratchpad和fop相关包。 - 转换DOCX到XSL-FO:
XWPFDocument doc = loadTemplateAndFillVariables(); OutputStream out = new ByteArrayOutputStream(); XWPFConverter.getInstance().convert(doc, out, null); String foContent = out.toString();
- 用FOP解析FO内容,获取页数:
FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI()); FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); ByteArrayInputStream foIn = new ByteArrayInputStream(foContent.getBytes()); // 设置不生成PDF,只做布局检查 foUserAgent.getRendererOptions().put("pdf-renderer", "org.apache.fop.render.pdf.PDFRenderer"); Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, new ByteArrayOutputStream()); TransformerFactory factory = TransformerFactory.newInstance(); Transformer transformer = factory.newTransformer(); Source src = new StreamSource(foIn); Result res = new SAXResult(fop.getDefaultHandler()); transformer.transform(src, res); // 获取总页数 int pageCount = foUserAgent.getResults().getPageCount(); if (pageCount > 1) { // 调整内容... }
方案三:从业务层限制内容长度
如果上面的技术方案太复杂,可以直接从业务逻辑入手:
- 给可变长度的变量设置最大字符限制,填充前判断长度,超过就自动截断(加上
...)。 - 动态调整字体大小:比如当内容过长时,把字号从11磅改成10磅,减少行高。
注意事项
- 不同的字体(比如宋体、微软雅黑)字符宽度和行高有差异,计算时最好用实际使用的字体测试。
- 表格的合并单元格、嵌套段落需要额外处理,避免高度计算错误。
- 文档如果有多个节,要确保获取的是当前内容所在节的页面设置。
内容的提问来源于stack exchange,提问作者Дима Годиков




