使用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,提问作者Міша Гожда
相关产品推荐
相关产品推荐

