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

使用Apache PDFBox 3.0.2提取并保留字体/坐标时特定中文字符出现错位

使用Apache PDFBox 3.0.2提取并保留字体/坐标时特定中文字符出现错位

我太懂你这种头疼的情况了——用PDFBox 3.0.2提取文本复刻到新PDF时,大部分内容都精准对位,偏生像“一年级”这类中文字符突然偏移个二三十单位,临时换本地Simhei字体虽然能解决眼前问题,但总担心换个PDF又出幺蛾子,得找到根儿上的解决办法才行。

先聊聊为啥会出现这个错位问题

从你贴的代码和打印的TextPosition信息来看,主要踩了这几个坑:

  1. 原字体复用逻辑有问题:原PDF里的中文字体大概率是Type0/CID字体,这类字体和文档资源绑定,你直接把原文档的字体对象加到backupFonts里,新文档其实没法正确识别和使用它,导致 fallback到替换字体时没继承原有的变换矩阵,位置直接跑偏。
  2. 文本矩阵计算画蛇添足:你自己手动计算旋转角度、重构变换矩阵的逻辑,对中文字体的字体矩阵(通常是极小的缩放系数)处理有误,反而把原本正确的坐标给算错了。
  3. 字体替换时丢了位置参数:catch块里替换字体的代码只设置了字体和字号,没复用原文本的TextMatrix,相当于用默认位置输出文本,可不就偏移了嘛。

给你几个针对性的解决步骤,从根儿上搞定它

1. 正确复用原PDF的字体资源,别自己瞎维护备份

原页面的字体是和页面资源绑定的,不能直接把原文档的字体对象拿到新文档用,得把字体资源复制到目标页面的资源里:

// 在处理每个页面时,复制原页面的字体资源到目标页面
PDResources sourceResources = sourcePage.getResources();
PDResources targetResources = targetPage.getResources();
for (COSName fontName : sourceResources.getFontNames()) {
    PDFont sourceFont = sourceResources.getFont(fontName);
    // 把原字体添加到目标页面的资源中,让新文档能正常访问
    targetResources.put(fontName, sourceFont);
}

这样你在设置contentStream.setFont的时候,就能直接用原字体,从源头上避免字体替换导致的坐标问题。

2. 删掉多余的矩阵计算,直接用TextPosition给的现成参数

TextPosition已经帮你算好了正确的文本矩阵和坐标,完全没必要自己手动算角度、拼矩阵。直接用现成的就行:

// 直接复用原文本的TextMatrix,位置、缩放、旋转全是对的
contentStream.setFont(position.getFont(), position.getFontSizeInPt());
contentStream.setTextMatrix(position.getTextMatrix());
contentStream.showText(position.getUnicode());

你之前的代码里手动concatenate字体矩阵、算角度,反而把中文字体的特殊矩阵给算错了,删掉这部分逻辑就好。

3. 字体替换时必须复用原文本的所有位置参数

真遇到原字体用不了的情况,替换字体时一定要把原文本的TextMatrix带上,不能只设置字体就输出:

catch (Exception e) {
    System.out.println("原字体用不了,尝试备用字体: " + e.getMessage());
    PDFont fft = getAvaliableFont(position.getUnicode(), backupFonts);
    if (fft != null) {
        // 核心:必须带上原文本的TextMatrix,确保位置和原文本完全一致
        contentStream.setTextMatrix(position.getTextMatrix());
        contentStream.setFont(fft, position.getFontSizeInPt());
        contentStream.showText(position.getUnicode());
    }
}

这样就算换了字体,文本的位置也和原文档丝毫不差。

4. 提前排查原PDF的字体情况

如果还是有问题,用PDFBox自带的PDFDebugger打开原PDF,看看“一年级”用的是什么字体——是不是嵌入的子集字体?编码是不是GB2312/UTF-16?搞清楚原字体的类型,能更精准地调整逻辑。

修正后的核心代码片段

给你整理了copyText2方法的修正版,直接用就行:

public static void copyText2(Set<PDFont> backupFonts, PDDocument source, int sourcePageNumber, PDDocument target, PDPage targetPage) throws IOException {
    List<TextPosition> allTextPositions = new ArrayList<>();
    PDFTextStripper pdfTextStripper = new PDFTextStripper() {
        @Override
        protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
            allTextPositions.addAll(textPositions);
            super.writeString(text, textPositions);
        }
    };
    pdfTextStripper.setStartPage(sourcePageNumber + 1);
    pdfTextStripper.setEndPage(sourcePageNumber + 1);
    pdfTextStripper.setSortByPosition(true);
    pdfTextStripper.getText(source);

    PDPage sourcePage = source.getPage(sourcePageNumber);
    PDResources sourceResources = sourcePage.getResources();
    PDResources targetResources = targetPage.getResources();

    // 复制原页面所有字体到目标页面资源
    for (COSName fontName : sourceResources.getFontNames()) {
        PDFont sourceFont = sourceResources.getFont(fontName);
        targetResources.put(fontName, sourceFont);
    }
    // 添加备用字体到目标页面资源
    for (PDFont backupFont : backupFonts) {
        COSName backupFontName = COSName.getPDFName("BackupFont_" + backupFont.getName());
        targetResources.put(backupFontName, backupFont);
    }

    PDPageContentStream contentStream = new PDPageContentStream(target, targetPage, PDPageContentStream.AppendMode.APPEND, true, true);
    contentStream.beginText();

    for (TextPosition position : allTextPositions) {
        try {
            // 直接用原字体和原文本矩阵,精准对位
            contentStream.setFont(position.getFont(), position.getFontSizeInPt());
            contentStream.setTextMatrix(position.getTextMatrix());
            contentStream.showText(position.getUnicode());
        } catch (Exception e) {
            System.out.println("无法使用原字体,尝试备用字体: " + e.getMessage());
            PDFont fft = getAvaliableFont(position.getUnicode(), backupFonts);
            if (fft != null) {
                // 替换字体时严格复用原文本的位置参数
                contentStream.setTextMatrix(position.getTextMatrix());
                contentStream.setFont(fft, position.getFontSizeInPt());
                contentStream.showText(position.getUnicode());
            }
        }
    }
    contentStream.endText();
    contentStream.close();
}

最后再啰嗦两句

这样修改后,不管是原字体还是替换字体,都能保证文本位置和原文档完全一致。另外,处理中文字体时一定要记住:字体资源和文档/页面绑定,不能跨文档直接复用对象;TextPosition已经给了所有正确的位置参数,别自己瞎折腾计算。

备注:内容来源于stack exchange,提问作者huanmierusi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 09:58:00