使用 PDFBox 编辑 pdf 中的内容会删除 pdf 中的最后一行

Editing content in pdf using PDFBox removes last line from pdf

我正在尝试在 Java 中使用 PDFBox 编辑 pdf 的一些内容。问题是,每当我编辑 pdf 中的任何字符串并尝试使用 Adob​​e Reader 打开它时,最后一行不会出现在新呈现的 pdf 中。

当我尝试直接从浏览器顶部打开呈现的 pdf 时,我能够看到最后一行。但是,它以不同的格式编码。我正在使用以下代码编辑 pdf 的内容:

PDDocument doc = PDDocument.load(FileName);
PDPage page = (PDPage) doc.getDocumentCatalog().getAllPages().get(0);
PDStream contents = page.getContents();
PDFStreamParser parser = new PDFStreamParser(contents.getStream());
parser.parse();
List<Object> tokens = parser.getTokens();
for (int j = 0; j < tokens.size(); j++) {
    Object next = tokens.get(j);
    if (next instanceof PDFOperator) {
        PDFOperator op = (PDFOperator) next;
        if (op.getOperation().equals("Tj")) {
            COSString previous = (COSString) tokens.get(j - 1);
            String string = previous.getString();

            string = string.replace("@ordnum&", (null != data.getOrderNumber()?data.getOrderNumber():""));
            string = string.replace("@shipid&", (null != data.getShipmentId()?data.getShipmentId():""));
            string = string.replace("@customer&", (null != data.getCustomerNumber()?data.getCustomerNumber():""));
            string = string.replace("@fromname&", (null != data.getFromName()?data.getFromName():""));

            tokens.set(j - 1, new COSString(string.trim()));
        }
    }
}

编辑 pdf 会删除显示 "Have questions? ..." 的行。这里有什么问题?我做错了什么吗?

谢谢。

为什么最后一行变得无效

首先你要知道PDF中的字符串有两种根本不同的情况[​​=30=]

  • 外部 内容流,例如文档属性的作者和关键字,以及
  • inside 表示要绘制的某种字体的字形序列的内容流。

前一种类型使用 PDFDocEncoding(类似于 Latin1)UTF 编码 -16BE 带有前导字节顺序标记。 方法 COSString.getString 和构造函数 COSString(String) 是为这种字符串设计的。

后一种类型使用为要呈现此字符串的 PDF 字体定义的编码进行编码。这可能是一些标准化编码,例如 WinAnsiEncoding(类似于 Latin1)或 UniGB-UTF16-H(Adobe 的 Unicode (UTF-16BE) 编码-GB1人物合集)。但它也可能是一些自定义的单字节或多字节编码。标准化和自定义多字节编码都没有字节顺序标记。

在 PDF 的页面内容流中,大多数字符串使用 WinAnsiEncoding(因为这是它们字体的编码)。因为 WinAnsiEncodingPDFDocEncoding 非常相似,所以您使用的 PDFDocEncoding COSString 方法和构造函数对他们来说工作得很好。

不过,最后一行是使用 Identity-H 编码的,它是 2 字节 CID 的水平身份映射 ,即直接引用字体程序中字符 ID 的两字节编码,如果没有该字体程序则没有任何意义。

由于这个字符串不以字节顺序标记开头,COSString.getString 假定它使用单字节编码 PDFDocEncoding 并因此创建两个 Java 每个原始两字节 PDF 字符串字符的字符串字符。由于其中一些字符的字符值超出实际有效的 PDFDocEncoding 范围,构造函数 COSString(String) 创建一个 PDF 字符串,其中每个中间 Java 字符使用一个两字节 UTF-16BE 字符表示;此外还添加了一个字节顺序标记。

于是,原始PDF字符串(十六进制写法)

002b004400590048000300540058004800560057004c0052005100560022000300260052
005100570044004600570003005800560003004400570003004b0057005700530056001d
00120012005a005a005a005600110046004c0057005500580056004f0044005100480011
004600520050001200460052005100570044004600570010005800560012

在您的编辑后成为

FEFF002B0000004400000059000000480000000300000054000000580000004800000056
000000570000004C00000052000000510000005600000022000000030000002600000052
000000510000005700000044000000460000005700000003000000580000005600000003
0000004400000057000000030000004B00000057000000570000005300000056000002DB
00000012000000120000005A0000005A0000005A0000005600000011000000460000004C
000000570000005500000058000000560000004F00000044000000510000004800000011
000000460000005200000050000000120000004600000052000000510000005700000044
0000004600000057000000100000005800000056

这可能会产生不同的效果,具体取决于 PDF 查看器。你原来的线路

例如可能会传播得很广:

或完全消失

因此,简而言之,如果您需要像这样编辑 PDF,请确保您只编辑具有类似 Latin1 编码的 PDF 字符串。

如果您还需要编辑不同编码的 PDF 字符串,请使用 COSString 方法 getBytes 将它们提取为 byte[],以适用于相关编码的方式编辑此数组,并使用构造函数 COSString(byte[]).

从已编辑的字节创建一个新的 COSString

但即便如此也不是一个好主意。

像一般那样编辑流的问题

像这样编辑流时还有许多其他陷阱等着你

  • 而不是例如

    (@customer&) Tj
    

    您的信息流可能包含

    (@cust) Tj
    (omer&) Tj
    

    [(@cust) -6 (omer&) ] TJ 
    

    甚至

    (omer&) Tj
    -62 0 Td
    (@cust) Tj
    

    因此,如果新模板使用稍微不同的表示,突然替换可能不起作用。

  • 只能部分嵌入字体。如果您替换的字符的字形不包括在内,它们将被绘制为间隙。

  • 在您编辑的操作之后的文本绘制操作可能指望前一个操作使用了特定的宽度。然后您的替换可以破坏以前的布局。

  • ...

本质上正确编辑通用文档中的流是非常困难的。

你还能做什么

您可以使用 AcroForm 表单字段,而不是像您的 @customer& 这样的内容占位符。

表单域是有名称的,可以被名称识别。填写它们不会改变内容中的任何内容。

如果您不希望其他人之后编辑您的 PDF 表单域,您可以将它们标记为只读,甚至可以将它们拼合到内容中。