You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何使用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可以实现这个需求,具体思路是:

  1. 收集所有出现过的字符(从上面的WordFontPair中提取)。
  2. 针对每个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等),不同类型的字体子集提取逻辑略有差异。

注意事项

  1. 部分PDF可能存在字体编码的问题,导致字符和字体的映射不准确,这时候需要额外处理编码转换(比如PDFont.encode()方法)。
  2. 如果PDF中的字体是子集化过的(即字体名称带有+前缀,比如+AAAAAA+Arial),说明原字体已经是子集,这时候提取的子集体积会更小。
  3. 确保使用最新版本的PDFBox和FontBox,避免旧版本的API兼容性问题。

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

火山引擎 最新活动