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

PDFBox写入新PDDocument时行显示异常问题求助

问题解决思路

问题1:每行单独占一页

你的核心逻辑错误在于将原文档的每页循环与新文档的页面创建绑定,导致原文档有多少页,新文档就生成多少页,且所有行都重复写入每个新页面(甚至因坐标重叠导致显示异常)。正确的逻辑应该是根据提取的文本内容分页,而非依赖原文档的页数。

问题2:文本超出宽度不自动换行

PDPageContentStream.showText()方法不会自动处理换行,必须手动计算文本宽度与页面可用宽度的关系,拆分长文本为多行。


修正后的完整代码实现

1. 文本提取与清理

// 从原文档提取文本
PDDocument oldDoc = PDDocument.load(new File("original.pdf"));
String text = new PDFTextStripper().getText(oldDoc);
String[] rawLines = text.split("\\r\\n");

// 清理控制字符并过滤空行
List<String> cleanedLines = new ArrayList<>();
for (String line : rawLines) {
    String cleaned = line.replaceAll("[\\r\\n]", "").trim();
    if (!cleaned.isEmpty()) {
        cleanedLines.add(cleaned);
    }
}
oldDoc.close();

2. 分页写入与自动换行核心逻辑

PDDocument newDoc = new PDDocument();
PDPage currentPage = new PDPage();
newDoc.addPage(currentPage);

PDPageContentStream contentStream = null;
PDFont font = PDType1Font.HELVETICA;
float fontSize = 12;
float leading = fontSize * 1.2f; // 行间距设为字体大小的1.2倍
float marginLeft = 50;
float marginTop = 700;
float pageWidth = currentPage.getMediaBox().getWidth() - marginLeft * 2; // 页面可用宽度
float currentY = marginTop; // 当前文本写入的Y坐标

try {
    contentStream = new PDPageContentStream(newDoc, currentPage);
    contentStream.setFont(font, fontSize);

    for (String line : cleanedLines) {
        // 将长文本拆分为符合页面宽度的多行
        List<String> wrappedLines = wrapText(line, font, fontSize, pageWidth);

        for (String wrappedLine : wrappedLines) {
            // 检查当前页面剩余空间是否足够写入下一行
            if (currentY - leading < 50) { // 底部边距设为50
                // 关闭当前页面的内容流
                contentStream.endText();
                contentStream.close();
                // 创建新页面并初始化内容流
                currentPage = new PDPage();
                newDoc.addPage(currentPage);
                contentStream = new PDPageContentStream(newDoc, currentPage);
                contentStream.setFont(font, fontSize);
                currentY = marginTop;
            }

            contentStream.beginText();
            contentStream.newLineAtOffset(marginLeft, currentY);

            // 处理H1标记内容
            if (wrappedLine.startsWith("1.")) {
                COSName h1Tag = COSName.getPDFName("H1");
                contentStream.beginMarkedContent(h1Tag);
                contentStream.showText(wrappedLine);
                contentStream.endMarkedContent();
            } else {
                contentStream.showText(wrappedLine);
            }

            contentStream.endText();
            currentY -= leading; // 下移行间距,准备写下一行
        }
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (contentStream != null) {
            contentStream.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

// 保存并关闭新文档
try {
    newDoc.save("marked.pdf");
    newDoc.close();
} catch (IOException e) {
    e.printStackTrace();
}

3. 文本换行工具方法

/**
 * 将长文本拆分为符合页面宽度的多行
 * @param text 待拆分的文本
 * @param font 使用的字体
 * @param fontSize 字体大小
 * @param maxWidth 每行最大宽度
 * @return 拆分后的行列表
 * @throws IOException 字体相关异常
 */
private static List<String> wrapText(String text, PDFont font, float fontSize, float maxWidth) throws IOException {
    List<String> resultLines = new ArrayList<>();
    StringBuilder currentLine = new StringBuilder();
    float currentLineWidth = 0;
    float spaceWidth = font.getStringWidth(" ") * fontSize / 1000; // 空格的宽度

    String[] words = text.split(" ");
    for (String word : words) {
        float wordWidth = font.getStringWidth(word) * fontSize / 1000;

        // 检查当前行添加该单词后是否超出宽度限制
        if (currentLineWidth + wordWidth + (currentLine.length() > 0 ? spaceWidth : 0) <= maxWidth) {
            if (currentLine.length() > 0) {
                currentLine.append(" ");
                currentLineWidth += spaceWidth;
            }
            currentLine.append(word);
            currentLineWidth += wordWidth;
        } else {
            // 当前行已满,写入结果并重置
            resultLines.add(currentLine.toString());
            currentLine = new StringBuilder(word);
            currentLineWidth = wordWidth;
        }
    }

    // 添加最后一行
    if (currentLine.length() > 0) {
        resultLines.add(currentLine.toString());
    }

    return resultLines;
}

关键修正点

  1. 解除原文档页数绑定:不再遍历原文档的每页,而是根据文本内容动态创建新页面
  2. 动态分页判断:通过监控当前Y坐标与底部边距的距离,自动创建新页面
  3. 手动文本换行:通过wrapText方法计算每个单词的宽度,拆分长文本为符合页面宽度的行
  4. 修正标记内容嵌套:确保beginMarkedContent与endMarkedContent正确配对,避免PDF结构错误

内容的提问来源于stack exchange,提问作者0xracer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 09:24:57