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

基于PDFBox更新PDF时的分词与字符显示问题咨询

问题描述

想咨询是否可通过PDFBox库对PDF指定位置进行更新。目前使用网上的Groovy方案,但getTokens()方法无法正确拆分词汇,导致难以定位需修改的内容。

尝试搜索“Hours worked”并替换为“Hours worked: 2”,遇到两个问题:

  • 执行后日志显示token生成异常:同一行的内容被拆分为两个不同的COSArray,导致难以精准搜索特定短语;
  • 找到目标短语并替换后,PDF中出现奇怪字符。

现寻求以下两个问题的解决方案:

  1. 如何设置解析器的token行为,将整段短语作为单个token直到遇到特殊字符?
  2. 如何对新PDF中的字符进行正确格式化?

现有Groovy代码

for( int i = 0; i < dataContext.getDataCount(); i++ ) {
InputStream is = dataContext.getStream(i);
Properties props = dataContext.getProperties(i);
String searchString= "Hours worked";
String replacement = "Hours worked: 2";
File file = new File("\\****\UKDC\GFS\PRE\PREPROD\Alchemer\Template\***.pdf"); 
PDDocument doc = PDDocument.load(file);  
for ( PDPage page : doc.getPages() )
    {
        PDFStreamParser parser = new PDFStreamParser(page);
        parser.parse();
        List tokens = parser.getTokens();
        logger.info("in Page");
        for (int j = 0; j < tokens.size(); j++) 
        {
            logger.info("tokens:"+tokens[j]);
            Object next = tokens.get(j);
             //logger.info("in Object");
             if (next instanceof Operator) 
             {
                Operator op = (Operator) next;
                String pstring = "";
                int prej = 0;
                
                //Tj and TJ are the two operators that display strings in a PDF
                if (op.getName().equals("Tj")) 
                {
                    logger.info("in Tj");
                    // Tj takes one operator and that is the string to display so lets update that operator
                    COSString previous = (COSString) tokens.get(j - 1);
                    String string = previous.getString();
                    logger.info("previousString:"+string);
                    string = string.replaceFirst(searchString, replacement);
                    previous.setValue(string.getBytes());
                } else 
                if (op.getName().equals("TJ")) 
                {
                    logger.info("in TJ:"+ op.getName());
                    COSArray previous = (COSArray) tokens.get(j - 1);
                    logger.info("previous:"+previous);
                    for (int k = 0; k < previous.size(); k++) 
                    {
                        Object arrElement = previous.getObject(k);
                        if (arrElement instanceof COSString) 
                        {
                            COSString cosString = (COSString) arrElement;
                            String string = cosString.getString();
                             logger.info("string:"+string);
                            if (j == prej || string.equals(" ") || string.equals(":") || string.equals("-")) {
                                pstring += string;
                            } else {
                                prej = j;
                                pstring = string;
                            }
                        }                       
                    }                        
                    logger.info("pstring:"+pstring);
                    if (searchString.equals(pstring.trim())) 
                    {  
                        logger.info("in searchString");
                        COSString cosString2 = (COSString) previous.getObject(0);
                        cosString2.setValue(replacement.getBytes());                           

                        int total = previous.size()-1;    
                        for (int k = total; k > 0; k--) {
                            previous.remove(k);
                        }                            
                    }
                }
            }
        }
        logger.info("in updatedStream");
        // now that the tokens are updated we will replace the page content stream.
        PDStream updatedStream = new PDStream(doc);
        OutputStream out = updatedStream.createOutputStream(COSName.FLATE_DECODE);
        ContentStreamWriter tokenWriter = new ContentStreamWriter(out);
        tokenWriter.writeTokens(tokens);            
        logger.info("in tokenWriter");
        out.close();
        page.setContents(updatedStream);
        
        doc.save("\\***\UKDC\GFS\PRE\PREPROD\Alchemer\***1.pdf");
    }
解决方案

问题1:让解析器将整段短语作为单个token

PDFBox的PDFStreamParser是严格按照PDF内容流语法拆分token的,无法直接设置让它把整段短语合并成单个token——因为PDF内容流里的文本本来就可能被拆分成多个COSString(比如为了调整字符间距、换行,或是生成PDF时的排版策略导致)。

解决思路是自己在解析时合并相邻文本token:

  • 遍历token列表时,跟踪当前文本片段,把属于同一语义块的COSString合并(比如在同一个文本对象BT/ET之间的连续文本);
  • 处理TJ操作符对应的COSArray时,先过滤掉用于字符间距调整的数字,把连续的COSString拼接成完整短语后再搜索匹配;
  • 匹配到目标短语后,不要直接替换第一个元素并删除其他元素,而是根据原有的间距控制重新构建COSArray,保留必要的排版信息。

问题2:解决替换后出现奇怪字符的问题

奇怪字符通常由编码不匹配或字体不支持导致,可通过以下方式解决:

  1. 修正编码问题:PDF文本使用的是字体编码(不一定是UTF-8),直接用string.getBytes()会用系统默认编码,导致编码不匹配。替换代码里的cosString2.setValue(replacement.getBytes())为cosString2.setValue(replacement),利用COSString的内置方法自动处理PDF字符串编码;
  2. 检查字体支持:确认原PDF的字体是否支持替换文本中的所有字符,若包含特殊符号,需确保字体覆盖对应字符集;
  3. 优化保存逻辑:原代码在循环每个页面时都调用doc.save(),会重复保存文档,建议将doc.save()移到所有页面处理完成后执行,避免不必要的IO操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 02:50:34