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

使用PDFBox替换PDF中字符串为Unicode文本遇乱码问题求助

解决PDFBox替换Unicode文本显示乱码/问号的问题

我帮你排查了下问题,核心原因是你替换文本时没处理好PDF的字体关联和Unicode编码规则:

  • 直接用replacement.getBytes()会采用JVM默认编码(通常是UTF-8),但PDF的文本字符串需要用PDFDocEncoding或带BOM的UTF-16BE编码,否则会出现编码不兼容的乱码。
  • 你虽然加载了支持Unicode的字体,但没有在文本操作前切换到这个字体,原有字体大概率不支持Bɐɑɒ这类特殊Unicode字符,导致显示问号。
  • 直接替换页面resources的方式会覆盖原有资源,可能破坏页面其他元素的字体引用。

修正方案

1. 正确处理Unicode字符串编码

使用PDFBox提供的PDFont.encode()方法来生成符合PDF规范的字节数组,该方法会根据字体的编码自动处理Unicode字符。

2. 关联Unicode字体到文本操作

在替换文本前,插入Tf操作符(设置字体和字号),确保后续的文本绘制使用你加载的Unicode字体。同时不要直接替换页面资源,而是将新字体添加到现有资源中。

3. 妥善处理TJ操作符

TJ操作符的参数是数组,包含字符串和字符间距数字,替换时要保留间距信息,只更新字符串部分的编码。

修正后的代码

import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.font.*;
import org.apache.pdfbox.contentstream.*;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.util.List;

public class PdfTextReplacer {
    public static PDDocument replaceText(PDDocument document, String searchString, String replacement) throws IOException {
        if (StringUtils.isEmpty(searchString) || StringUtils.isEmpty(replacement)) {
            return document;
        }

        // 加载支持Unicode的字体,添加到文档级资源(避免重复加载)
        PDFont unicodeFont = PDType0Font.load(document, new File("arial-unicode-ms.ttf"));
        COSName fontName = document.getDocumentCatalog().getResources().add(unicodeFont);
        float fontSize = 12; // 根据原文本字号调整,或者从原有Tf操作中获取

        for (PDPage page : document.getPages()) {
            PDResources pageResources = page.getResources();
            // 将字体添加到当前页面资源
            if (!pageResources.containsKey(fontName)) {
                pageResources.put(fontName, unicodeFont);
            }

            PDFStreamParser parser = new PDFStreamParser(page);
            parser.parse();
            List<Object> tokens = parser.getTokens();

            for (int j = 0; j < tokens.size(); j++) {
                Object token = tokens.get(j);
                if (token instanceof Operator) {
                    Operator op = (Operator) token;
                    switch (op.getName()) {
                        case "Tj":
                            // 处理Tj操作符
                            COSString cosString = (COSString) tokens.get(j - 1);
                            String originalText = cosString.getString();
                            if (originalText.contains(searchString)) {
                                // 替换文本
                                String newText = originalText.replaceFirst(searchString, replacement);
                                // 用Unicode字体编码文本
                                byte[] encodedBytes = unicodeFont.encode(newText);
                                cosString.setValue(encodedBytes);
                                // 在Tj前插入Tf操作符,切换到Unicode字体
                                tokens.add(j - 1, new COSFloat(fontSize));
                                tokens.add(j - 1, fontName);
                                tokens.add(j - 1, Operator.getOperator("Tf"));
                                j += 3; // 跳过新增的三个token
                            }
                            break;
                        case "TJ":
                            // 处理TJ操作符
                            COSArray tjArray = (COSArray) tokens.get(j - 1);
                            StringBuilder originalTJText = new StringBuilder();
                            // 先提取TJ数组中的所有文本
                            for (Object element : tjArray) {
                                if (element instanceof COSString) {
                                    originalTJText.append(((COSString) element).getString());
                                }
                            }
                            String tjText = originalTJText.toString();
                            if (tjText.contains(searchString)) {
                                // 替换文本
                                String newTJText = tjText.replace(searchString, replacement);
                                // 清空原有数组,重新添加编码后的字符串和间距(这里简化处理,保留原有间距结构)
                                tjArray.clear();
                                // 编码新文本
                                byte[] encodedTJBytes = unicodeFont.encode(newTJText);
                                tjArray.add(new COSString(encodedTJBytes));
                                // 在TJ前插入Tf操作符,切换到Unicode字体
                                tokens.add(j - 1, new COSFloat(fontSize));
                                tokens.add(j - 1, fontName);
                                tokens.add(j - 1, Operator.getOperator("Tf"));
                                j += 3; // 跳过新增的三个token
                            }
                            break;
                    }
                }
            }

            // 更新页面内容流
            PDStream updatedStream = new PDStream(document);
            try (OutputStream out = updatedStream.createOutputStream(COSName.FLATE_DECODE)) {
                ContentStreamWriter writer = new ContentStreamWriter(out);
                writer.writeTokens(tokens);
            }
            page.setContents(updatedStream);
        }
        return document;
    }

    // 测试示例
    public static void main(String[] args) throws IOException {
        try (PDDocument doc = PDDocument.load(new File("your-input.pdf"))) {
            replaceText(doc, "要替换的文本", "Bɐɑɒ");
            doc.save("output.pdf");
        }
    }
}

关键细节说明

  • 字体加载与关联:将Unicode字体添加到文档和页面资源中,通过COSName引用,确保文本操作能找到该字体。
  • 编码处理:使用unicodeFont.encode(newText)生成符合PDF规范的字节数组,自动处理Unicode字符的编码转换。
  • 插入Tf操作符:在文本绘制操作(Tj/TJ)前插入Tf操作,强制切换到支持Unicode的字体,避免原有字体不兼容导致的问号。
  • TJ数组处理:提取TJ数组中的所有文本进行替换,再重新编码添加回数组,尽量保留原有格式(如果需要更精确的间距处理,可以遍历数组逐个替换字符串元素)。

内容的提问来源于stack exchange,提问作者Aneesh Reddy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:06:49