Apache PDFBox:编码问题
Apache PDFBox: problems with encoding
我有一个 PDF 模板并试图替换其中的一些单词。我使用此代码:
private PDDocument replaceText(PDDocument document, String searchString, String replacement) throws IOException {
if (searchString.isEmpty() || replacement.isEmpty()) {
return document;
}
PDPageTree pages = document.getDocumentCatalog().getPages();
for (PDPage page : pages) {
PDFStreamParser parser = new PDFStreamParser(page);
parser.parse();
List<Object> tokens = parser.getTokens();
for (int j = 0; j < tokens.size(); j++) {
Object next = tokens.get(j);
if (next instanceof Operator) {
Operator op = (Operator) next;
//Tj and TJ are the two operators that display strings in a PDF
if (op.getName().equals("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();
if (searchString.equals(string)) {
System.out.println(string);
}
string = string.replaceFirst(searchString, replacement);
previous.setValue(string.getBytes());
} else if (op.getName().equals("TJ")) {
COSArray previous = (COSArray) tokens.get(j - 1);
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();
if (searchString.equals(string)) {
System.out.println(string);
}
string = StringUtils.replaceOnce(string, searchString, replacement);
cosString.setValue(string.getBytes());
}
}
}
}
}
// now that the tokens are updated we will replace the page content stream.
PDStream updatedStream = new PDStream(document);
OutputStream out = updatedStream.createOutputStream();
ContentStreamWriter tokenWriter = new ContentStreamWriter(out);
tokenWriter.writeTokens(tokens);
page.setContents(updatedStream);
out.close();
}
return document;
}
我的 PDF 模板只有 3 个字符串:"file:///C/Users/Mi/Downloads/converted.txt"、“[10.03.2020 18:43:57]”和 "hello!!!"。
前 2 个字符串搜索正确,但第三个看起来像 "KHOOR...":
据我了解,编码不匹配。当我尝试用 "Hello!" 替换 "file:///C/Users/Mi/Downloads/converted.txt" 时,它替换为 "ello",不显示大写字母和标记。据我了解,主要区别在于字体。 "hello"有字体设置,其他没有。
源 PDF 在这里:
https://yadi.sk/i/l0OAcFkAkUHKYg
求教,如何从 PDF 中获取文本作为正确的字符串并进行替换。
这个答案实际上解释了为什么针对您的任务的通用解决方案即使不是不可能也至少非常复杂。在良性情况下,即对于受特定限制的 PDF,可以成功使用像您这样的代码,但您的示例 PDF 表明您显然想要操作的 PDF 不受那样的限制。
为什么自动替换文字是difficult/impossible
有许多因素会阻碍自动替换 PDF 中的文本,有些因素已经使 查找 绘制相关文本的说明变得困难,有些因素使 替换这些指令参数中的字符。
这里列出的问题并不详尽!
查找绘制特定文本的说明
PDF 包含内容流,其中包含指示 PDF 处理器在何处绘制内容的指令序列。 PDF 中的常规文本是通过设置当前字体(和字体大小)、设置绘制文本的位置以及实际绘制文本的指令绘制的。这可以像这样易于理解和搜索:
/TT0 1 Tf
9 0 0 9 5 5 Tm
(file:///C/Users/Mi/Downloads/converted.txt[10.03.2020 18:43:57]) Tj
(这里选择字体TT0,大小为1,然后仿射变换将文本缩放9倍,移动到位置(5, 5) , 最后画出文字 "file:///C/Users/Mi/Downloads/converted.txt [10.03.2020 18:43:57]".)
在这种情况下,搜索负责绘制给定文本的指令很容易。但相关说明也可能看起来不同。
分割线
例如,字符串可能被分段绘制,而不是上面的Tj指令,我们可能有
[(file:///C/Users/Mi/Downloads/converted.txt)2 ([10.03.2020 18:43:57])] TJ
(这里先画"file:///C/Users/Mi/Downloads/converted.txt",然后稍微移动文字画位置,然后" [10.03.2020 18:43:57] 被绘制,两者在相同的 TJ 指令中。)
或者您可能会看到
(file:///C/Users/Mi/Downloads/converted.txt) Tj
([10.03.2020 18:43:57]) Tj
(不同指令绘制的文字部分。)
此外,文本片段的顺序可能出乎意料:
([10.03.2020 18:43:57]) Tj
-40 0 Td
(file:///C/Users/Mi/Downloads/converted.txt) Tj
(先绘制日期字符串,然后文本位置在绘制日期之前向左移动一点,绘制URL。)
一些 PDF 制作者分别绘制每个字符,在两者之间设置整个文本转换:
9 0 0 9 5 5 Tm
(f) Tj
9 0 0 9 14 5 Tm
(i) Tj
9 0 0 9 23 5 Tm
(l) Tj
...
并且这些不同的指令不需要像这里那样按顺序排列,它们可以分布在整个流中,甚至分布在多个流中,因为一个页面可以有一系列内容流,而不是单个或部分内容流可以在从页面内容流引用的子对象的内容流中绘制字符串。
因此,为了找到负责特定多字符文本的指令,您可能必须检查多个流并根据绘制的位置将找到的字符串粘合在一起。
连字
并非每个字符代码都对应于搜索字符串中的单个字符。有许多特殊字形用于字符组合,例如 fl
用于 fl
等。因此为了搜索,必须扩展此类连字。
编码
在上面的例子中,即使文字不是单独绘制的,文字的字符也很容易识别运行。但在 PDF 中,字符的编码不需要那么明显,实际上每个字体都有自己的编码,例如
<004B0048004F004F0052000400040004>Tj
会画画“你好!!!”.
(这里的字符串参数写成十六进制字符串,在调试器中你看到了"KHOOR..."。)
因此,在查找文本时,首先需要根据当前字体的具体编码,将文本绘制指令的字符串参数映射到Unicode。
但 PDF 不需要包含从单个代码到 Unicode 字符的映射,可能只有到字体文件中字形 ID 的映射。在嵌入字体文件的情况下,这些字体文件也不需要包含任何到 Unicode 字符的映射。
通常 PDF 文件确实包含有关与代码匹配的 Unicode 字符的信息,以允许文本提取,例如对于 copy/paste;但严格来说,此类信息是可选的;更糟糕的是,当 显示 PDF 时,该信息可能包含错误而不会产生问题。在所有这些情况下,必须使用类似 OCR 的机制来识别与每个字形相关联的 Unicode 字符。
替换说明中的文字
一旦找到负责绘制所搜索文本的说明,就必须替换文本。这也可能暗示着一些问题。
子集字体
如果字体文件嵌入到 PDF 中,它们通常只是作为原始字体的子集嵌入以保存 space。例如。在您的示例 PDF 中,Tahoma 字体用于显示“hello!!!”仅嵌入了以下字形:
Even Times New Roman(用于您可以识别的文本的字体)只是嵌入了以下字形的子集:
这样,即使你找到了“hello!!!”在 Tahoma 中,只需将字符代码替换为“再见??”只会显示“ e e ”,因为嵌入字体中存在字形的唯一字符是 'e'.
因此,要进行替换,您可能必须编辑嵌入字体文件和代表 PDF 字体对象以包含和编码所有必需的字形,或者添加另一种字体和说明以切换到该字体以进行操作的文本绘图说明,然后再返回。
字体编码
即使您的字体根本没有嵌入(因此将使用您的字体的完整本地副本)或嵌入了您需要的所有字形,用于字体的编码也可能受到限制。在基于西欧语言的 PDF 中,您经常会发现 WinAnsiEncoding,一种类似于 Windows 代码页 1252 的编码。如果要替换为西里尔文本,则没有字符代码那些字符。
因此,在这种情况下,您可能必须更改编码以包含您需要的所有字符(通过扫描所有使用的相关字体来查找当前编码中未使用的字符)或添加另一种更合适的字体编码。
布局注意事项
如果替换文本比替换文本长或短,并且 PDF 中的同一行后面还有其他文本,则您必须决定是否也应移动该文本。它可能属于一起并且必须相应地移动,但它也可能来自单独的文本块或列,在这种情况下不应移动它。
文本对齐也可能损坏。
还要考虑标记文本(下划线/删除线/背景色/...)。 PDF 中的这些标记(通常)不是字体属性,而是单独的矢量图形。为了使这些正确,您必须解析页面中的矢量图形和注释,启发式地识别文本标记,并更新它们。
带标签的 PDF
如果您处理带标签的 PDF(例如为了可访问性),这可能会使查找 文本更容易(因为可访问性应该允许轻松提取文本)但替换文本会更难,因为您可能还必须更新一些标签或结构树数据。
如何实现通用文本替换
如上所示,PDF 中的文本替换存在很多障碍。因此,一个完整的解决方案(如果可能的话)远远超出了堆栈溢出答案的范围。不过有一些建议:
要找到要替换的文本,您应该使用 PdfTextStripper
(用于文本提取的 PDFBox 实用程序 class)并将其扩展为所有带有指向分别绘制每个字符的文本绘制指令的文本。这样你就不必实现所有的文本解码和排序。
要替换文本,您可以询问 PDFBox 字体 classes(如果相应扩展,则由 PdfTextStripper
提供)是否可以对您的替换进行编码文字.
手头总是有一份 PDF 规范(ISO 32000-1 或 ISO 32000-2)...
但请注意,您需要一段时间(数周或数月)才能获得比较不错的通用解决方案。
我有一个 PDF 模板并试图替换其中的一些单词。我使用此代码:
private PDDocument replaceText(PDDocument document, String searchString, String replacement) throws IOException {
if (searchString.isEmpty() || replacement.isEmpty()) {
return document;
}
PDPageTree pages = document.getDocumentCatalog().getPages();
for (PDPage page : pages) {
PDFStreamParser parser = new PDFStreamParser(page);
parser.parse();
List<Object> tokens = parser.getTokens();
for (int j = 0; j < tokens.size(); j++) {
Object next = tokens.get(j);
if (next instanceof Operator) {
Operator op = (Operator) next;
//Tj and TJ are the two operators that display strings in a PDF
if (op.getName().equals("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();
if (searchString.equals(string)) {
System.out.println(string);
}
string = string.replaceFirst(searchString, replacement);
previous.setValue(string.getBytes());
} else if (op.getName().equals("TJ")) {
COSArray previous = (COSArray) tokens.get(j - 1);
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();
if (searchString.equals(string)) {
System.out.println(string);
}
string = StringUtils.replaceOnce(string, searchString, replacement);
cosString.setValue(string.getBytes());
}
}
}
}
}
// now that the tokens are updated we will replace the page content stream.
PDStream updatedStream = new PDStream(document);
OutputStream out = updatedStream.createOutputStream();
ContentStreamWriter tokenWriter = new ContentStreamWriter(out);
tokenWriter.writeTokens(tokens);
page.setContents(updatedStream);
out.close();
}
return document;
}
我的 PDF 模板只有 3 个字符串:"file:///C/Users/Mi/Downloads/converted.txt"、“[10.03.2020 18:43:57]”和 "hello!!!"。 前 2 个字符串搜索正确,但第三个看起来像 "KHOOR...":
据我了解,编码不匹配。当我尝试用 "Hello!" 替换 "file:///C/Users/Mi/Downloads/converted.txt" 时,它替换为 "ello",不显示大写字母和标记。据我了解,主要区别在于字体。 "hello"有字体设置,其他没有。
源 PDF 在这里: https://yadi.sk/i/l0OAcFkAkUHKYg
求教,如何从 PDF 中获取文本作为正确的字符串并进行替换。
这个答案实际上解释了为什么针对您的任务的通用解决方案即使不是不可能也至少非常复杂。在良性情况下,即对于受特定限制的 PDF,可以成功使用像您这样的代码,但您的示例 PDF 表明您显然想要操作的 PDF 不受那样的限制。
为什么自动替换文字是difficult/impossible
有许多因素会阻碍自动替换 PDF 中的文本,有些因素已经使 查找 绘制相关文本的说明变得困难,有些因素使 替换这些指令参数中的字符。
这里列出的问题并不详尽!
查找绘制特定文本的说明
PDF 包含内容流,其中包含指示 PDF 处理器在何处绘制内容的指令序列。 PDF 中的常规文本是通过设置当前字体(和字体大小)、设置绘制文本的位置以及实际绘制文本的指令绘制的。这可以像这样易于理解和搜索:
/TT0 1 Tf
9 0 0 9 5 5 Tm
(file:///C/Users/Mi/Downloads/converted.txt[10.03.2020 18:43:57]) Tj
(这里选择字体TT0,大小为1,然后仿射变换将文本缩放9倍,移动到位置(5, 5) , 最后画出文字 "file:///C/Users/Mi/Downloads/converted.txt [10.03.2020 18:43:57]".)
在这种情况下,搜索负责绘制给定文本的指令很容易。但相关说明也可能看起来不同。
分割线
例如,字符串可能被分段绘制,而不是上面的Tj指令,我们可能有
[(file:///C/Users/Mi/Downloads/converted.txt)2 ([10.03.2020 18:43:57])] TJ
(这里先画"file:///C/Users/Mi/Downloads/converted.txt",然后稍微移动文字画位置,然后" [10.03.2020 18:43:57] 被绘制,两者在相同的 TJ 指令中。)
或者您可能会看到
(file:///C/Users/Mi/Downloads/converted.txt) Tj
([10.03.2020 18:43:57]) Tj
(不同指令绘制的文字部分。)
此外,文本片段的顺序可能出乎意料:
([10.03.2020 18:43:57]) Tj
-40 0 Td
(file:///C/Users/Mi/Downloads/converted.txt) Tj
(先绘制日期字符串,然后文本位置在绘制日期之前向左移动一点,绘制URL。)
一些 PDF 制作者分别绘制每个字符,在两者之间设置整个文本转换:
9 0 0 9 5 5 Tm
(f) Tj
9 0 0 9 14 5 Tm
(i) Tj
9 0 0 9 23 5 Tm
(l) Tj
...
并且这些不同的指令不需要像这里那样按顺序排列,它们可以分布在整个流中,甚至分布在多个流中,因为一个页面可以有一系列内容流,而不是单个或部分内容流可以在从页面内容流引用的子对象的内容流中绘制字符串。
因此,为了找到负责特定多字符文本的指令,您可能必须检查多个流并根据绘制的位置将找到的字符串粘合在一起。
连字
并非每个字符代码都对应于搜索字符串中的单个字符。有许多特殊字形用于字符组合,例如 fl
用于 fl
等。因此为了搜索,必须扩展此类连字。
编码
在上面的例子中,即使文字不是单独绘制的,文字的字符也很容易识别运行。但在 PDF 中,字符的编码不需要那么明显,实际上每个字体都有自己的编码,例如
<004B0048004F004F0052000400040004>Tj
会画画“你好!!!”.
(这里的字符串参数写成十六进制字符串,在调试器中你看到了"KHOOR..."。)
因此,在查找文本时,首先需要根据当前字体的具体编码,将文本绘制指令的字符串参数映射到Unicode。
但 PDF 不需要包含从单个代码到 Unicode 字符的映射,可能只有到字体文件中字形 ID 的映射。在嵌入字体文件的情况下,这些字体文件也不需要包含任何到 Unicode 字符的映射。
通常 PDF 文件确实包含有关与代码匹配的 Unicode 字符的信息,以允许文本提取,例如对于 copy/paste;但严格来说,此类信息是可选的;更糟糕的是,当 显示 PDF 时,该信息可能包含错误而不会产生问题。在所有这些情况下,必须使用类似 OCR 的机制来识别与每个字形相关联的 Unicode 字符。
替换说明中的文字
一旦找到负责绘制所搜索文本的说明,就必须替换文本。这也可能暗示着一些问题。
子集字体
如果字体文件嵌入到 PDF 中,它们通常只是作为原始字体的子集嵌入以保存 space。例如。在您的示例 PDF 中,Tahoma 字体用于显示“hello!!!”仅嵌入了以下字形:
Even Times New Roman(用于您可以识别的文本的字体)只是嵌入了以下字形的子集:
这样,即使你找到了“hello!!!”在 Tahoma 中,只需将字符代码替换为“再见??”只会显示“ e e ”,因为嵌入字体中存在字形的唯一字符是 'e'.
因此,要进行替换,您可能必须编辑嵌入字体文件和代表 PDF 字体对象以包含和编码所有必需的字形,或者添加另一种字体和说明以切换到该字体以进行操作的文本绘图说明,然后再返回。
字体编码
即使您的字体根本没有嵌入(因此将使用您的字体的完整本地副本)或嵌入了您需要的所有字形,用于字体的编码也可能受到限制。在基于西欧语言的 PDF 中,您经常会发现 WinAnsiEncoding,一种类似于 Windows 代码页 1252 的编码。如果要替换为西里尔文本,则没有字符代码那些字符。
因此,在这种情况下,您可能必须更改编码以包含您需要的所有字符(通过扫描所有使用的相关字体来查找当前编码中未使用的字符)或添加另一种更合适的字体编码。
布局注意事项
如果替换文本比替换文本长或短,并且 PDF 中的同一行后面还有其他文本,则您必须决定是否也应移动该文本。它可能属于一起并且必须相应地移动,但它也可能来自单独的文本块或列,在这种情况下不应移动它。
文本对齐也可能损坏。
还要考虑标记文本(下划线/删除线/背景色/...)。 PDF 中的这些标记(通常)不是字体属性,而是单独的矢量图形。为了使这些正确,您必须解析页面中的矢量图形和注释,启发式地识别文本标记,并更新它们。
带标签的 PDF
如果您处理带标签的 PDF(例如为了可访问性),这可能会使查找 文本更容易(因为可访问性应该允许轻松提取文本)但替换文本会更难,因为您可能还必须更新一些标签或结构树数据。
如何实现通用文本替换
如上所示,PDF 中的文本替换存在很多障碍。因此,一个完整的解决方案(如果可能的话)远远超出了堆栈溢出答案的范围。不过有一些建议:
要找到要替换的文本,您应该使用 PdfTextStripper
(用于文本提取的 PDFBox 实用程序 class)并将其扩展为所有带有指向分别绘制每个字符的文本绘制指令的文本。这样你就不必实现所有的文本解码和排序。
要替换文本,您可以询问 PDFBox 字体 classes(如果相应扩展,则由 PdfTextStripper
提供)是否可以对您的替换进行编码文字.
手头总是有一份 PDF 规范(ISO 32000-1 或 ISO 32000-2)...
但请注意,您需要一段时间(数周或数月)才能获得比较不错的通用解决方案。