You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 21:15:25