如何使用PDFBox获取PDF文件中每个单词的字体文件或PDFont结构并提取目标字体子集?
解决PDF中单词与字体映射及字体子集提取问题
你当前的代码确实只能获取页面级的所有字体,但没办法把字体和具体的单词/字符关联起来。要实现每个单词对应字体的需求,我们可以通过自定义PDFTextStripper子类来拦截每个字符的渲染信息,再把字符组合成单词并绑定对应的字体。下面是具体实现步骤:
一、获取每个单词对应的字体
PDFTextStripper是PDFBox中处理文本提取的核心类,我们可以重写它的processTextPosition方法——这个方法会在每个字符被解析时触发,每个TextPosition对象包含了字符的字体、位置、内容等关键信息。
实现代码示例
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class WordFontMapper extends PDFTextStripper { // 存储当前正在组合的单词及对应的字体 private StringBuilder currentWord = new StringBuilder(); private PDFont currentFont; // 存储最终的单词-字体映射结果 private List<WordFontPair> wordFontPairs = new ArrayList<>(); public WordFontMapper() throws IOException { super(); } @Override protected void processTextPosition(TextPosition text) { String character = text.getUnicode(); PDFont font = text.getFont(); // 判断当前字符是否属于单词(简单判断:非空白字符) if (!character.trim().isEmpty()) { // 如果当前单词为空,或者字符和当前单词字体一致,继续组合 if (currentWord.length() == 0 || currentFont.equals(font)) { currentWord.append(character); currentFont = font; } else { // 字体变化,保存当前单词,开始新单词 wordFontPairs.add(new WordFontPair(currentWord.toString(), currentFont)); currentWord = new StringBuilder(character); currentFont = font; } } else { // 遇到空白字符,保存当前单词(如果有的话) if (currentWord.length() > 0) { wordFontPairs.add(new WordFontPair(currentWord.toString(), currentFont)); currentWord.setLength(0); currentFont = null; } } } // 页面处理结束后,检查是否有未保存的单词 @Override protected void writeString(String string, List<TextPosition> textPositions) throws IOException { super.writeString(string, textPositions); if (currentWord.length() > 0) { wordFontPairs.add(new WordFontPair(currentWord.toString(), currentFont)); currentWord.setLength(0); currentFont = null; } } // 获取最终的单词-字体映射列表 public List<WordFontPair> getWordFontPairs() { return wordFontPairs; } // 内部类,存储单词和对应的字体 public static class WordFontPair { private String word; private PDFont font; public WordFontPair(String word, PDFont font) { this.word = word; this.font = font; } public String getWord() { return word; } public PDFont getFont() { return font; } } // 测试使用 public static void main(String[] args) { try (PDDocument document = PDDocument.load(new File("xxofd.pdf"))) { WordFontMapper mapper = new WordFontMapper(); mapper.getText(document); // 遍历输出单词和对应的字体 for (WordFontPair pair : mapper.getWordFontPairs()) { System.out.printf("单词:%s,字体名称:%s,字体类型:%s%n", pair.getWord(), pair.getFont().getName(), pair.getFont().getType()); } } catch (IOException e) { e.printStackTrace(); } } }
关键说明
TextPosition.getFont()会直接返回当前字符对应的PDFont对象,这就是你需要的字体结构。- 单词的拆分逻辑这里用了简单的空白字符判断,如果需要更精准的分词(比如连字符、特殊符号),可以根据实际需求调整判断逻辑。
- 注意处理页面结束时的剩余单词,避免遗漏。
二、提取PDF中实际用到的字体子集
要缩小字体文件体积,核心是提取仅包含PDF中出现过的字符的字体子集。PDFBox结合FontBox可以实现这个需求,具体思路是:
- 收集所有出现过的字符(从上面的
WordFontPair中提取)。 - 针对每个
PDFont,如果是嵌入式字体,提取其原始字体数据,然后生成只包含目标字符的子集;如果是非嵌入式字体,需要找到对应的系统字体再生成子集。
实现代码示例(嵌入式字体子集提取)
import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.fontbox.ttf.TrueTypeFont; import org.apache.fontbox.ttf.TTFSubsetter; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class FontSubsetExtractor { // 提取字体子集,保存为新的TTF文件 public static void extractFontSubset(PDFont font, Set<String> usedCharacters, String outputPath) throws IOException { // 处理Type0字体(最常见的嵌入式字体类型) if (font instanceof PDType0Font) { PDType0Font type0Font = (PDType0Font) font; TrueTypeFont ttf = type0Font.getTrueTypeFont(); if (ttf != null) { TTFSubsetter subsetter = new TTFSubsetter(ttf); // 添加所有用到的字符 for (String c : usedCharacters) { subsetter.addChar(c.codePointAt(0)); } // 生成子集并保存 try (FileOutputStream fos = new FileOutputStream(outputPath)) { subsetter.writeToStream(fos); } } } // 处理Type1字体(较少见,需要额外处理) else if (font instanceof PDType1Font) { PDType1Font type1Font = (PDType1Font) font; // Type1字体的子集提取逻辑相对复杂,可参考FontBox的Type1相关API实现 // 这里仅做示例提示,实际需要根据字体数据处理 System.out.println("Type1字体子集提取需额外实现"); } else { System.out.println("不支持的字体类型:" + font.getType()); } } // 测试使用:从WordFontPair中收集所有字符 public static void main(String[] args) throws IOException { // 假设已经通过WordFontMapper获取了wordFontPairs列表 List<WordFontMapper.WordFontPair> wordFontPairs = ...; // 按字体分组,收集每个字体对应的所有字符 Map<PDFont, Set<String>> fontCharMap = new HashMap<>(); for (WordFontMapper.WordFontPair pair : wordFontPairs) { PDFont font = pair.getFont(); String word = pair.getWord(); fontCharMap.computeIfAbsent(font, k -> new HashSet<>()); for (int i = 0; i < word.length(); i++) { fontCharMap.get(font).add(String.valueOf(word.charAt(i))); } } // 为每个字体生成子集 int index = 0; for (Map.Entry<PDFont, Set<String>> entry : fontCharMap.entrySet()) { PDFont font = entry.getKey(); Set<String> chars = entry.getValue(); extractFontSubset(font, chars, "subset_font_" + index + ".ttf"); index++; } } }
关键说明
- 仅嵌入式字体可以直接从PDF中提取原始字体数据生成子集;非嵌入式字体(比如系统字体)需要先找到对应的本地字体文件,再用FontBox生成子集。
TTFSubsetter是FontBox提供的工具类,专门用于生成TrueType字体的子集,能有效缩小字体文件体积。- 注意处理不同类型的字体(Type0、Type1等),不同类型的字体子集提取逻辑略有差异。
注意事项
- 部分PDF可能存在字体编码的问题,导致字符和字体的映射不准确,这时候需要额外处理编码转换(比如
PDFont.encode()方法)。 - 如果PDF中的字体是子集化过的(即字体名称带有
+前缀,比如+AAAAAA+Arial),说明原字体已经是子集,这时候提取的子集体积会更小。 - 确保使用最新版本的PDFBox和FontBox,避免旧版本的API兼容性问题。
内容的提问来源于stack exchange,提问作者Serendipity




