pdf_transcribe.py
4.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#!/usr/bin/env python3
"""S12/S13 PDF 文本层解码转写(benchmark_v2 附录 B 口径 3 / R-G5)。
对象扫描 → FlateDecode 解压 → /ToUnicode CMap 解析(beginbfchar/beginbfrange)
→ 逐 Page 取 /Resources /Font 名到 CMap 的映射,解其 /Contents 内容流,
把 <CID> Tj / [...] TJ 映射回 Unicode → 输出转写文本。
用法: pdf_transcribe.py <pdf> # 全文转写到 stdout
pdf_transcribe.py <pdf> <q1> <q2> # 逐条报告空白不敏感命中数
"""
import re, sys, zlib
def objects(data):
out = {}
for m in re.finditer(rb'(\d+)\s+(\d+)\s+obj(.*?)endobj', data, re.S):
body = m.group(3)
sm = re.search(rb'stream\r?\n(.*?)endstream', body, re.S)
stream = sm.group(1) if sm else b''
out[int(m.group(1))] = {'raw': body[:sm.start()] if sm else body, 'stream': stream}
return out
def inflate(stream):
s = stream
if s.endswith(b'\r\n'):
s = s[:-2]
elif s.endswith(('\n'.encode(), b'\r')):
s = s[:-1]
try:
return zlib.decompress(s)
except Exception:
try:
return zlib.decompressobj().decompress(s)
except Exception:
return b''
def parse_cmap(data):
mp = {}
for m in re.finditer(rb'beginbfchar(.*?)endbfchar', data, re.S):
for pm in re.finditer(rb'<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>', m.group(1)):
mp[int(pm.group(1), 16)] = bytes.fromhex(pm.group(2).decode()).decode('utf-16-be', 'replace')
for m in re.finditer(rb'beginbfrange(.*?)endbfrange', data, re.S):
for pm in re.finditer(rb'<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>', m.group(1)):
lo, hi, base = int(pm.group(1), 16), int(pm.group(2), 16), pm.group(3).decode()
half = len(base) // 2
for i in range(hi - lo + 1):
mp[lo + i] = bytes.fromhex('%0*x' % (half * 2, int(base, 16) + i)).decode('utf-16-be', 'replace')
return mp
def fonts_map(objs):
ref2cmap = {}
for num, o in objs.items():
if b'/ToUnicode' in o['raw']:
tm = re.search(rb'/ToUnicode\s+(\d+)\s+0\s+R', o['raw'])
if tm and int(tm.group(1)) in objs:
ref2cmap[num] = parse_cmap(inflate(objs[int(tm.group(1))]['stream']))
return ref2cmap
def transcribe(path):
objs = objects(objects.__self__ if False else load(path))
ref2cmap = fonts_map(objs)
out = []
for num, o in objs.items():
raw = o['raw']
if b'/Type' in raw and b'/Page' not in raw:
continue
if b'/Contents' not in raw:
continue
# 字体名 → CMap(Resources 可能在 Page 自身或其继承的 Resources 对象里)
cur = {}
res = raw
rm = re.search(rb'/Resources\s+(\d+)\s+0\s+R', raw)
if rm and int(rm.group(1)) in objs:
res = objs[int(rm.group(1))]['raw']
for fm in re.finditer(rb'/(\w+)\s+(\d+)\s+0\s+R', res.split(b'/Font')[-1] if b'/Font' in res else res):
ref = int(fm.group(2))
if ref in ref2cmap:
cur[fm.group(1).decode()] = ref2cmap[ref]
if not cur:
continue
cm = re.search(rb'/Contents\s+(\d+)\s+0\s+R', raw)
if not (cm and int(cm.group(1)) in objs):
continue
content = inflate(objs[int(cm.group(1))]['stream'])
if not content:
continue
page, active = [], None
for tm in re.finditer(rb'/(\w+)\s+[\d.]+\s+Tf|<([0-9A-Fa-f]+)>\s*Tj|\[((?:[^\[\]]|\\.)*)\]\s*TJ|(T\*|Td|TD|TJ)', content, re.S):
if tm.group(1):
active = cur.get(tm.group(1).decode())
elif tm.group(2) and active:
page.append(active.get(int(tm.group(2), 16), ''))
elif tm.group(3) is not None and active:
for hm in re.finditer(rb'<([0-9A-Fa-f]+)>', tm.group(3)):
page.append(active.get(int(hm.group(1), 16), ''))
elif tm.group(4) in (b'T*', b'Td', b'TD'):
page.append('\n')
if page:
out.append(''.join(page))
return '\n'.join(out)
def load(path):
return open(path, 'rb').read()
def norm(s):
return re.sub(r'\s+', '', s)
if __name__ == '__main__':
txt = transcribe(sys.argv[1])
if len(sys.argv) == 2:
sys.stdout.write(txt)
sys.exit(0)
nt = norm(txt)
print(f'# {sys.argv[1]}: 转写 {len(txt)} 字符(空白不敏感 {len(nt)})')
for q in sys.argv[2:]:
print(f' {nt.count(norm(q))}\t{q}')