如何从PDF内容中提取(十六进制编码的)文本?

3
我有两个版本的PDF文件,我知道它们略有不同——第3页上灰色条中的“重新评估”文本:

online PDF diff

我正在尝试在我的计算机上获取文本差异。
我使用 pdfcpu 从多页 PDF 中提取内容,然后通过 diff 工具运行第三页。
% diff out_orig/page_3.txt out_new/page_3.txt 

1650a1651,1658
> BT
> 1 0 0 rg
> 0 i 
> /RelativeColorimetric ri
> /C2_2 9.96 Tf
> 0 Tw 358.147 648.779 Td
> <0035004800440056005600480056005600500048005100570003003000580056005700030032004600460058005500030028005900480055005C0003001600030030005200510057004B0056>Tj
> ET

我在PDF参考文献中查找了7.3.4.3 十六进制字符串

一个十六进制字符串应该被写作一系列用ASCII字符编码的十六进制数字,同时被尖括号所包围。

因此,我认为我应该能够直接将十六进制字符解释为ASCII文本。
>>> s = '0035004800440056005600480056005600500048005100570003003000580056005700030032004600460058005500030028005900480055005C0003001600030030005200510057004B0056'
>>> import binascii
>>> binascii.a2b_hex(s)
b'\x005\x00H\x00D\x00V\x00V\x00H\x00V\x00V\x00P\x00H\x00Q\x00W\x00\x03\x000\x00X\x00V\x00W\x00\x03\x002\x00F\x00F\x00X\x00U\x00\x03\x00(\x00Y\x00H\x00U\x00\\\x00\x03\x00\x16\x00\x03\x000\x00R\x00Q\x00W\x00K\x00V'

但是我得到的是垃圾。即使没有空字节:
>>> binascii.a2b_hex(s).replace(b'\x00', b'')
b'5HDVVHVVPHQW\x030XVW\x032FFXU\x03(YHU\\\x03\x16\x030RQWKV'

我希望它看起来像这样(反过来):
>>> binascii.b2a_hex(b'Reassessment Must Occur Every 3 Months')
b'52656173736573736d656e74204d757374204f636375722045766572792033204d6f6e746873'

我在这个某个相关的SO帖子上找到了这条评论:

字面字符串(7.3.4.2)- 这非常直观,因为你只需遍历“( . ? )”的数据* - 这仅适用于使用标准字体编码的简单示例。与此同时,嵌入字体的自定义编码已经变得非常普遍。

所以...也许这个十六进制字符串不仅仅是十六进制编码的ASCII码? 我在提取文本差异方面漏掉了什么?

1
对我来说,十六进制字符串中不包含任何"abcdef"字节是很奇怪的。虽然这种情况可能发生,但对于这么长的字符串来说,这种可能性非常小。我认为这不是一个十六进制字符串。 - Michael Ruth
2
这是UTF-16编码。每个字符占用两个字节。 - Mark Reed
1
哦,它不是UTF-16编码。我觉得可能是一种自定义的编码,不过需要查看原始PDF文件才能找到相关信息。 - wim
1
@wim:感谢您对此进行评论。您能指出一些资源帮助我挖掘这个问题,或者更好地理解问题空间吗?另外,您的解决方案中的+29偏移量是从哪里来的?您是自己看到的吗? - Zach Young
2
十六进制字符串并不是您所期望的文本字符串,双字节十六进制代码是字体glyf表中的字形索引。PDF中的/C2_2字体对象应包括一个ToUnicode cmap对象,将字形索引映射到实际字符。通常,字体生成器将字形放置在glyf表中,与字符相同的顺序(至少对于英文字母中的那些字符),因此如果您可以猜测偏移量(即特定于字体的偏移量),例如29,则可以对某些字符进行“暴力”映射。 - iPDFdev
显示剩余3条评论
2个回答

4

我们开始吧:

>>> s = '0035004800440056005600480056005600500048005100570003003000580056005700030032004600460058005500030028005900480055005C0003001600030030005200510057004B0056'
>>> ns = [29 + int(c, 16) for c in chunks(s, 4)]
>>> print(bytes(ns))
b'Reassessment Must Occur Every 3 Months'

chunks is copied from here.


4
可以,但是这个29位的偏移码从哪里来的呢? - Mark Reed
你也可以直接使用 ''.join([chr(29+int(s[i:i+4],16)) for i in range(0,len(s),4)]),不需要 chunks 的定义。 - Mark Reed
1
有时候可以用,有时候就不行。编码取决于所涉及的PDF字体。而且那个PDF字体可能有着相当任意的编码方式。 - mkl

3
不,它不是ASCII编码。ASCII编码仅限于8位。
多字节字符代码用于pdf的复合字体,并通过其在字形表中的索引指定要绘制的字形。实质上没有字符映射。有一个从这些字形索引到Unicode的反向映射,以使文本搜索成为可能。
常见的OpenType字体格式要求字形索引0 = .notdef,1 = .null,2 = CR和3 = space(ASCII代码32)。请注意,32-3 = 29。
因此,为ASCII字符集创建的OpenType复合字体,缺少非打印字符0至31将具有以下属性: 字形索引+29 = ASCII

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接