You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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转换器对段落的换行规则、字符间距处理不够精准,尤其是文档包含自定义段落格式或特殊字体时,容易出现多余换行。

解决方案

  1. 升级依赖版本:确保使用最新稳定版的POI(如5.2.5+)和对应的fr.opensagres.xdocreport.converter(2.0.2+),新版本修复了不少排版bug。
  2. 预处理文档段落:遍历文档段落,移除不必要的换行标记,统一段落格式。
  3. 配置字体映射:避免因字体缺失导致的排版错乱。

优化后的代码

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默认的分页策略会将无法在当前页完整显示的元素整体移到下一页,而表格标题作为独立段落,未和表格设置“保持在一起”的属性,导致两者被拆分到不同页面。

解决方案

  1. 设置段落的“keep with next”属性:让表格标题和后续表格保持在同一页。
  2. 设置表格的“keep lines”属性:避免表格自身被分页拆分。
  3. 调整页面边距:增加当前页的可容纳空间。

优化后的代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 10:57:36