使用PDFBox(Kotlin)修改PDF文本失效:仅部分模板生效问题
PDFBox文本替换失效排查与解决方案
我用Kotlin基于PDFBox库修改PDF文本,替换目标内容的功能在一份模板上正常运行,但在另一份模板上完全无法生效,核心替换页面文本的代码如下:
private fun replacePage(page: PDPage, page2Stream: PDPageContentStream) { var font = PDType1Font.HELVETICA_BOLD val parser = PDFStreamParser(page) parser.parse() val tokens = parser.tokens for (tokenIndex in tokens.indices) { val token = tokens[tokenIndex] if (token !is Operator) { continue } when (token.name) { OperatorName.BEGIN_TEXT -> { page2Stream.beginText() } OperatorName.MOVE_TEXT -> { val x = (tokens[tokenIndex - 2] as COSNumber).floatValue() val y = (tokens[tokenIndex - 1] as COSNumber).floatValue() page2Stream.newLineAtOffset(x, y) } OperatorName.SET_MATRIX -> { val ary = COSArray() ary.addAll((6 downTo 1).map { tokens[tokenIndex - it] as COSNumber }) page2Stream.setTextMatrix(Matrix(ary)) } OperatorName.SET_FONT_AND_SIZE -> { val fontNameObj = tokens[tokenIndex - 2] as COSName val fontSizeObj = tokens[tokenIndex - 1] as COSNumber font = PDType1Font.HELVETICA_BOLD val fontSize = fontSizeObj.floatValue() page2Stream.setFont(font!!, fontSize) } OperatorName.SHOW_TEXT -> { val previous = tokens[tokenIndex - 1] as COSString val buffer = parsePdfString(font!!, previous) val buffer2 = replacePdfText(buffer) page2Stream.showText(buffer2) } OperatorName.SHOW_TEXT_ADJUSTED -> { val previous = tokens[tokenIndex - 1] as COSArray val buffer = previous.filterIsInstance<COSString>().joinToString("") { obj -> parsePdfString(font!!, obj) } val buffer2 = replacePdfText(buffer) page2Stream.showText(buffer2) } OperatorName.END_TEXT -> { page2Stream.endText() } OperatorName.MOVE_TO -> { val x = (tokens[tokenIndex - 2] as COSNumber).floatValue() val y = (tokens[tokenIndex - 1] as COSNumber).floatValue() page2Stream.moveTo(x, y) } OperatorName.LINE_TO -> { val x = (tokens[tokenIndex - 2] as COSNumber).floatValue() val y = (tokens[tokenIndex - 1] as COSNumber).floatValue() page2Stream.lineTo(x, y) } OperatorName.STROKE_PATH -> { page2Stream.stroke() } } } }
失效原因排查及修复方案
1. 未覆盖所有文本绘制操作符
PDF文本绘制除了SHOW_TEXT(Tj)和SHOW_TEXT_ADJUSTED(TJ),还有SHOW_TEXT_LINE(T)、SHOW_TEXT_LINE_AND_SPACE(T*)两个操作符,当前代码未处理,导致这两类文本无法被替换。需要补充对应分支:
OperatorName.SHOW_TEXT_LINE -> { val previous = tokens[tokenIndex - 1] as COSString val buffer = parsePdfString(font!!, previous) val buffer2 = replacePdfText(buffer) page2Stream.showText(buffer2) } OperatorName.SHOW_TEXT_LINE_AND_SPACE -> { val textStr = tokens[tokenIndex - 2] as COSString val spaceAdjust = tokens[tokenIndex - 1] as COSNumber val buffer = parsePdfString(font!!, textStr) val buffer2 = replacePdfText(buffer) page2Stream.showTextWithSpaceAdjustment(buffer2, spaceAdjust.floatValue()) }
2. 硬编码字体导致渲染异常
代码中强制将字体设置为PDType1Font.HELVETICA_BOLD,如果目标PDF使用自定义嵌入字体、非Type1字体,会导致替换后的文本无法正确渲染(甚至完全不显示)。应保留原字体:
OperatorName.SET_FONT_AND_SIZE -> { val fontNameObj = tokens[tokenIndex - 2] as COSName val fontSizeObj = tokens[tokenIndex - 1] as COSNumber // 从页面资源中获取原字体, fallback到默认字体 val resources = page.resources font = resources.getFont(fontNameObj) ?: PDType1Font.HELVETICA_BOLD val fontSize = fontSizeObj.floatValue() page2Stream.setFont(font!!, fontSize) }
3. 缺失图形矩阵变换处理
部分PDF通过cm(图形矩阵)操作符变换文本位置,而非仅用Tm(文本矩阵),当前代码未处理cm,会导致替换后的文本位置偏移,看起来像是未替换。需要补充处理:
OperatorName.CONCAT_MATRIX -> { val matrixValues = (5 downTo 0).map { (tokens[tokenIndex - it] as COSNumber).floatValue() } page2Stream.transform(Matrix( matrixValues[0], matrixValues[1], matrixValues[2], matrixValues[3], matrixValues[4], matrixValues[5] )) }
4. 未处理Form XObject中的文本
如果失效PDF的文本存储在Form XObject中,当前代码仅解析页面主内容流,无法触及XObject内的文本。需要递归解析所有Form XObject:
// 在replacePage函数开头添加XObject处理逻辑 val resources = page.resources val xObjects = resources.getXObjects() xObjects.values.forEach { xObj -> if (xObj is PDFormXObject) { val xObjParser = PDFStreamParser(xObj) xObjParser.parse() // 复用现有token处理逻辑解析XObject内容流 page2Stream.saveGraphicsState() page2Stream.transform(xObj.matrix) // 建议将token处理逻辑提取为单独函数避免重复代码 processTokens(xObjParser.tokens, page2Stream, font) page2Stream.restoreGraphicsState() } }
5. 字符串解析逻辑错误
自定义的parsePdfString可能未正确处理PDF字符串编码(如WinAnsi、UTF-16BE),导致解析出的文本与原PDF不一致,替换逻辑匹配失败。建议改用PDFBox自带方法:
// 替换parsePdfString调用 val buffer = previous.getString()
6. 间接内容流解析问题
部分PDF的页面内容流是间接对象,PDFStreamParser(page)可能无法完整解析,需要手动获取完整内容流:
val contents = page.contents if (contents is PDStream) { val parser = PDFStreamParser(contents) parser.parse() val tokens = parser.tokens // 后续处理逻辑不变 }
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

