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

Posted

技术标签:

【中文标题】使用 PDFBox 编辑 pdf 中的内容会从 pdf 中删除最后一行【英文标题】:Editing content in pdf using PDFBox removes last line from pdf 【发布时间】:2015-03-23 20:54:04 【问题描述】:

我正在尝试使用 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 会删除显示“有问题?...”的行。这里有什么问题?我做错了吗?

谢谢。

【问题讨论】:

我做错了什么吗? - 是的:您认为COSString 的内容是易于编辑的字符串。一般来说,他们不是。它们可能具有自定义的单字节或多字节编码。在您的情况下,底线是唯一使用特殊编码进行编码的底线。而你的编辑会破坏它, 但是我没有对那个 COSString 做任何事情。当我尝试打印字符串时,我看不到那条线。那么它不应该按原样出现吗?如果没有,我该如何处理/处理?我是 PDFBox 新手,所以对它了解不多。 但我没有对那个 COSString 做任何事情 - 是的,你做了。您将其转换为 Java 字符串 (previous.getString()),您可以对其进行修剪并从 (new COSString(string.trim())) 构建一个新的 COSString,以替换原始字符串。在某些编码的情况下,这可能会完全破坏字符串。 如果没有,我该如何处理/处理 - PDF 内容根本不应该像这样编辑,无论是在 PDFBox 还是在其他 PDF 库中。考虑使用 PDF 表单域。 【参考方案1】:

为什么最后一行无效

首先你必须知道,PDF 中的字符串有两种根本不同的情况

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

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

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

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

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

由于此字符串不以字节顺序标记开头,COSString.getString 假定它使用单字节编码 PDFDocEncoding,因此为每个原始的两字节 PDF 字符串创建两个 Java 字符串字符特点。由于其中一些字符的字符值超出了实际有效的 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从使用构造函数 COSString(byte[]) 编辑的字节。

但即使这样也不是一个好主意。

一般情况下编辑流的问题

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

而不是例如

(@customer&) Tj

您的信息流可能包含

(@cust) Tj
(omer&) Tj

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

甚至

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

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

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

您编辑的文本绘制操作可能依赖于前一个使用特定宽度。然后你的替换可以破坏以前的布局。

...

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

你还能做什么

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

表单字段具有名称并且可以被它们识别。填写它们不会改变内容中的任何内容。

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

【讨论】:

非常感谢您提供详细解释的答案。 好久没问这个问题了,还有一个问题。正如您所建议的,我使用 AcroFroms 填充动态文本并使其只读。问题是,当我将 pdf 作为电子邮件发送时,iphone / ipad 上的人无法看到来自内容。看起来这是一个已知问题 (forums.adobe.com/thread/1216563)。对此有何建议? 扁平化 pdf 有帮助吗?我找不到任何用于展平 pdf 的好代码。你能给我提供任何扁平化pdf的链接吗?我查看了此链接***.com/questions/14454387/…,但它们都没有为我正常工作。 扁平化表单会有所帮助,是的,但我还没有使用 pdfbox 做到这一点。你的表单元素有外观流吗? 我不确定那是什么。我有带有样式(例如字体和大小)的空测试字段,我用纯文本替换。我看到 PDFClown 内置了扁平化支持。但这从系统上的文件中读取。我有一个字节数组,它看起来不像 PDFClowm 支持直接从字节读取。

以上是关于使用 PDFBox 编辑 pdf 中的内容会从 pdf 中删除最后一行的主要内容,如果未能解决你的问题,请参考以下文章

使用 PDFBOX 填写 PDF 表单中的多个字段并在填写后锁定编辑 pdf 文档

用 Java 中的 PDFbox 替换或删除 PDF 中的文本

如何用java读取pdf文档的部分内容

Java 使用PDFBox提取PDF文件中的图片

使用pdfbox分页保存pdf为图片

使用 PDFBox 生成的 PDF 为空白