使用Apache PDFBox 3.0.2提取并保留字体/坐标时特定中文字符出现错位
我太懂你这种头疼的情况了——用PDFBox 3.0.2提取文本复刻到新PDF时,大部分内容都精准对位,偏生像“一年级”这类中文字符突然偏移个二三十单位,临时换本地Simhei字体虽然能解决眼前问题,但总担心换个PDF又出幺蛾子,得找到根儿上的解决办法才行。
先聊聊为啥会出现这个错位问题
从你贴的代码和打印的TextPosition信息来看,主要踩了这几个坑:
- 原字体复用逻辑有问题:原PDF里的中文字体大概率是Type0/CID字体,这类字体和文档资源绑定,你直接把原文档的字体对象加到
backupFonts里,新文档其实没法正确识别和使用它,导致 fallback到替换字体时没继承原有的变换矩阵,位置直接跑偏。 - 文本矩阵计算画蛇添足:你自己手动计算旋转角度、重构变换矩阵的逻辑,对中文字体的字体矩阵(通常是极小的缩放系数)处理有误,反而把原本正确的坐标给算错了。
- 字体替换时丢了位置参数:catch块里替换字体的代码只设置了字体和字号,没复用原文本的
TextMatrix,相当于用默认位置输出文本,可不就偏移了嘛。
给你几个针对性的解决步骤,从根儿上搞定它
1. 正确复用原PDF的字体资源,别自己瞎维护备份
原页面的字体是和页面资源绑定的,不能直接把原文档的字体对象拿到新文档用,得把字体资源复制到目标页面的资源里:
// 在处理每个页面时,复制原页面的字体资源到目标页面 PDResources sourceResources = sourcePage.getResources(); PDResources targetResources = targetPage.getResources(); for (COSName fontName : sourceResources.getFontNames()) { PDFont sourceFont = sourceResources.getFont(fontName); // 把原字体添加到目标页面的资源中,让新文档能正常访问 targetResources.put(fontName, sourceFont); }
这样你在设置contentStream.setFont的时候,就能直接用原字体,从源头上避免字体替换导致的坐标问题。
2. 删掉多余的矩阵计算,直接用TextPosition给的现成参数
TextPosition已经帮你算好了正确的文本矩阵和坐标,完全没必要自己手动算角度、拼矩阵。直接用现成的就行:
// 直接复用原文本的TextMatrix,位置、缩放、旋转全是对的 contentStream.setFont(position.getFont(), position.getFontSizeInPt()); contentStream.setTextMatrix(position.getTextMatrix()); contentStream.showText(position.getUnicode());
你之前的代码里手动concatenate字体矩阵、算角度,反而把中文字体的特殊矩阵给算错了,删掉这部分逻辑就好。
3. 字体替换时必须复用原文本的所有位置参数
真遇到原字体用不了的情况,替换字体时一定要把原文本的TextMatrix带上,不能只设置字体就输出:
catch (Exception e) { System.out.println("原字体用不了,尝试备用字体: " + e.getMessage()); PDFont fft = getAvaliableFont(position.getUnicode(), backupFonts); if (fft != null) { // 核心:必须带上原文本的TextMatrix,确保位置和原文本完全一致 contentStream.setTextMatrix(position.getTextMatrix()); contentStream.setFont(fft, position.getFontSizeInPt()); contentStream.showText(position.getUnicode()); } }
这样就算换了字体,文本的位置也和原文档丝毫不差。
4. 提前排查原PDF的字体情况
如果还是有问题,用PDFBox自带的PDFDebugger打开原PDF,看看“一年级”用的是什么字体——是不是嵌入的子集字体?编码是不是GB2312/UTF-16?搞清楚原字体的类型,能更精准地调整逻辑。
修正后的核心代码片段
给你整理了copyText2方法的修正版,直接用就行:
public static void copyText2(Set<PDFont> backupFonts, PDDocument source, int sourcePageNumber, PDDocument target, PDPage targetPage) throws IOException { List<TextPosition> allTextPositions = new ArrayList<>(); PDFTextStripper pdfTextStripper = new PDFTextStripper() { @Override protected void writeString(String text, List<TextPosition> textPositions) throws IOException { allTextPositions.addAll(textPositions); super.writeString(text, textPositions); } }; pdfTextStripper.setStartPage(sourcePageNumber + 1); pdfTextStripper.setEndPage(sourcePageNumber + 1); pdfTextStripper.setSortByPosition(true); pdfTextStripper.getText(source); PDPage sourcePage = source.getPage(sourcePageNumber); PDResources sourceResources = sourcePage.getResources(); PDResources targetResources = targetPage.getResources(); // 复制原页面所有字体到目标页面资源 for (COSName fontName : sourceResources.getFontNames()) { PDFont sourceFont = sourceResources.getFont(fontName); targetResources.put(fontName, sourceFont); } // 添加备用字体到目标页面资源 for (PDFont backupFont : backupFonts) { COSName backupFontName = COSName.getPDFName("BackupFont_" + backupFont.getName()); targetResources.put(backupFontName, backupFont); } PDPageContentStream contentStream = new PDPageContentStream(target, targetPage, PDPageContentStream.AppendMode.APPEND, true, true); contentStream.beginText(); for (TextPosition position : allTextPositions) { try { // 直接用原字体和原文本矩阵,精准对位 contentStream.setFont(position.getFont(), position.getFontSizeInPt()); contentStream.setTextMatrix(position.getTextMatrix()); contentStream.showText(position.getUnicode()); } catch (Exception e) { System.out.println("无法使用原字体,尝试备用字体: " + e.getMessage()); PDFont fft = getAvaliableFont(position.getUnicode(), backupFonts); if (fft != null) { // 替换字体时严格复用原文本的位置参数 contentStream.setTextMatrix(position.getTextMatrix()); contentStream.setFont(fft, position.getFontSizeInPt()); contentStream.showText(position.getUnicode()); } } } contentStream.endText(); contentStream.close(); }
最后再啰嗦两句
这样修改后,不管是原字体还是替换字体,都能保证文本位置和原文档完全一致。另外,处理中文字体时一定要记住:字体资源和文档/页面绑定,不能跨文档直接复用对象;TextPosition已经给了所有正确的位置参数,别自己瞎折腾计算。
备注:内容来源于stack exchange,提问作者huanmierusi

