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

使用PDFBox 2.0.8重建PDF文档时遭遇No glyph for参数异常

解决PDFBox使用嵌入子集字体写入新文档时的No glyph for...异常

我之前碰到过一模一样的问题,罪魁祸首是嵌入字体的子集特性——PDF里的嵌入字体几乎都是只包含文档实际用到的字形的「精简版」,而不是完整的字体文件。当你直接用提取到的Unicode文本调用showText()时,PDFBox会尝试把Unicode字符映射到这个子集字体的字形,但只要原文档没用到这个字符(或者字体的编码映射有缺陷),就会抛出No glyph for...异常。

下面是两种可行的解决方案,按推荐优先级排序:

方案1:使用原始字符编码而非Unicode文本写入

嵌入子集字体只认它自己的自定义编码,而不是标准Unicode。所以我们需要从TextPosition中提取原始的字符编码字节数组,直接用这个数组调用showText(),绕过Unicode转码的步骤。

步骤1:扩展PDFTextStripper保存编码信息

import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class FontAwareStripper extends PDFTextStripper {
    private final List<TextSegment> textSegments = new ArrayList<>();

    public FontAwareStripper() throws IOException {
        super();
    }

    @Override
    protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
        for (TextPosition tp : textPositions) {
            // 保存字体、原始字符编码、字号和位置
            textSegments.add(new TextSegment(
                    tp.getFont(),
                    tp.getCharacterCodes(),
                    tp.getFontSize(),
                    tp.getX(),
                    tp.getY()
            ));
        }
    }

    public List<TextSegment> getTextSegments() {
        return textSegments;
    }

    // 辅助类存储文本段的完整信息
    public static class TextSegment {
        private final PDFont font;
        private final byte[] characterCodes;
        private final float fontSize;
        private final float x;
        private final float y;

        public TextSegment(PDFont font, byte[] characterCodes, float fontSize, float x, float y) {
            this.font = font;
            this.characterCodes = characterCodes;
            this.fontSize = fontSize;
            this.x = x;
            this.y = y;
        }

        // Getter方法省略
        public PDFont getFont() { return font; }
        public byte[] getCharacterCodes() { return characterCodes; }
        public float getFontSize() { return fontSize; }
        public float getX() { return x; }
        public float getY() { return y; }
    }
}

步骤2:用原始编码写入新PDF

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import java.io.IOException;

public class PdfCleaner {
    public static void main(String[] args) throws IOException {
        // 加载原始PDF
        try (PDDocument originalDoc = PDDocument.load(new File("original.pdf"))) {
            // 提取带编码的文本段
            FontAwareStripper stripper = new FontAwareStripper();
            stripper.stripPages(originalDoc, 1, originalDoc.getNumberOfPages());
            List<FontAwareStripper.TextSegment> segments = stripper.getTextSegments();

            // 创建新PDF并写入内容
            try (PDDocument cleanDoc = new PDDocument()) {
                PDPage page = new PDPage();
                cleanDoc.addPage(page);
                try (PDPageContentStream contentStream = new PDPageContentStream(cleanDoc, page)) {
                    contentStream.beginText();
                    for (FontAwareStripper.TextSegment segment : segments) {
                        contentStream.setFont(segment.getFont(), segment.getFontSize());
                        contentStream.newLineAtOffset(segment.getX(), segment.getY());
                        // 使用原始字符编码写入,而非Unicode文本
                        contentStream.showText(segment.getCharacterCodes());
                    }
                    contentStream.endText();
                }
                cleanDoc.save("cleaned.pdf");
            }
        }
    }
}

方案2:直接绘制字形路径(极端情况备用)

如果方案1仍然失效(比如原字体的编码映射完全损坏),可以跳过字体编码,直接提取字形的矢量路径并绘制到新文档中。这种方式不依赖字体的编码映射,完全基于字形的几何信息。

// 在写入内容时替换为以下逻辑
contentStream.beginText();
for (FontAwareStripper.TextSegment segment : segments) {
    PDFont font = segment.getFont();
    float fontSize = segment.getFontSize();
    float x = segment.getX();
    float y = segment.getY();

    contentStream.newLineAtOffset(x, y);
    for (byte code : segment.getCharacterCodes()) {
        int glyphId = font.codeToGID(code);
        GeneralPath glyphPath = font.getGlyphPath(glyphId);
        if (glyphPath != null) {
            // 缩放路径到对应字号
            glyphPath.transform(AffineTransform.getScaleInstance(fontSize / 1000, fontSize / 1000));
            contentStream.appendRawCommands(glyphPath.toPathString());
            
            // 计算字符宽度,移动到下一个字符位置
            float advance = font.getWidth(code) / 1000 * fontSize;
            contentStream.newLineAtOffset(advance, 0);
        }
    }
}
contentStream.endText();

额外建议

  • 如果不需要严格保留原字体外观,可以考虑替换为系统中可用的完整字体(比如PDType0Font.load(cleanDoc, new File("path/to/your/font.ttf"))),这样彻底避免子集字体的限制。
  • 对于恶意PDF,可能存在嵌入的无效字体子集,此时建议跳过异常字体,用通用 fallback 字体(如宋体、Arial)替代。

内容的提问来源于stack exchange,提问作者Міша Гожда

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 07:18:47