You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何计算Apache POI+Docx模板生成的单页DOCX文档高度?

解决Apache POI + templ4docx生成单页DOCX避免分页的问题

我来帮你搞定这个头疼的问题——用templ4docx和Apache POI生成单页DOCX时,填充变量后莫名多出第二页确实很烦人,而且POI在文档没被实际渲染前,确实没法直接拿到准确的行数(就像你遇到的,getLines()返回0)。下面给你几个实用的解决方案,亲测有效:

方案一:手动计算内容总高度(基于模板元素属性)

既然你的模板只有XWPFParagraphXWPFTable,我们可以直接遍历这些元素,结合它们的格式属性来计算总高度,再和页面可用高度对比,提前判断是否会分页。

具体步骤:

  1. 获取页面可用高度
    先拿到文档的页面设置,算出实际能放内容的高度:
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;
  1. 遍历元素计算高度
  • 对于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到总高度...
    }
    
  1. 判断是否超页
    把所有元素的高度累加后,如果超过usableHeight,就调整内容(比如缩小字号、减小行距、精简文本)。

方案二:借助XSL-FO转换获取渲染后布局

如果手动计算太麻烦,可以把DOCX转换成XSL-FO,用Apache FOP来获取实际渲染后的页面信息,判断是否会分页:

  1. 引入依赖:需要poi-ooxml-fullpoi-scratchpadfop相关包。
  2. 转换DOCX到XSL-FO:
XWPFDocument doc = loadTemplateAndFillVariables();
OutputStream out = new ByteArrayOutputStream();
XWPFConverter.getInstance().convert(doc, out, null);
String foContent = out.toString();
  1. 用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,提问作者Дима Годиков

火山引擎 最新活动