BabelDOC 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- babeldoc/__init__.py +1 -0
- babeldoc/asynchronize/__init__.py +51 -0
- babeldoc/const.py +7 -0
- babeldoc/converter.py +510 -0
- babeldoc/document_il/__init__.py +43 -0
- babeldoc/document_il/backend/__init__.py +0 -0
- babeldoc/document_il/backend/pdf_creater.py +402 -0
- babeldoc/document_il/frontend/__init__.py +0 -0
- babeldoc/document_il/frontend/il_creater.py +348 -0
- babeldoc/document_il/il_version_1.py +799 -0
- babeldoc/document_il/il_version_1.rnc +141 -0
- babeldoc/document_il/il_version_1.rng +390 -0
- babeldoc/document_il/il_version_1.xsd +235 -0
- babeldoc/document_il/midend/__init__.py +0 -0
- babeldoc/document_il/midend/add_debug_information.py +138 -0
- babeldoc/document_il/midend/il_translator.py +553 -0
- babeldoc/document_il/midend/layout_parser.py +180 -0
- babeldoc/document_il/midend/paragraph_finder.py +452 -0
- babeldoc/document_il/midend/remove_descent.py +168 -0
- babeldoc/document_il/midend/styles_and_formulas.py +701 -0
- babeldoc/document_il/midend/typesetting.py +761 -0
- babeldoc/document_il/translator/__init__.py +0 -0
- babeldoc/document_il/translator/cache.py +145 -0
- babeldoc/document_il/translator/translator.py +267 -0
- babeldoc/document_il/utils/fontmap.py +227 -0
- babeldoc/document_il/utils/layout_helper.py +467 -0
- babeldoc/document_il/utils/style_helper.py +86 -0
- babeldoc/document_il/xml_converter.py +51 -0
- babeldoc/docvision/README.md +0 -0
- babeldoc/docvision/__init__.py +0 -0
- babeldoc/docvision/doclayout.py +278 -0
- babeldoc/docvision/rpc_doclayout.py +162 -0
- babeldoc/high_level.py +468 -0
- babeldoc/main.py +466 -0
- babeldoc/pdfinterp.py +464 -0
- babeldoc/progress_monitor.py +146 -0
- babeldoc/translation_config.py +156 -0
- babeldoc-0.1.6.dist-info/METADATA +283 -0
- babeldoc-0.1.6.dist-info/RECORD +42 -0
- babeldoc-0.1.6.dist-info/WHEEL +4 -0
- babeldoc-0.1.6.dist-info/entry_points.txt +2 -0
- babeldoc-0.1.6.dist-info/licenses/LICENSE +661 -0
babeldoc/pdfinterp.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pdfminer import settings
|
|
8
|
+
from pdfminer.pdfcolor import PREDEFINED_COLORSPACE
|
|
9
|
+
from pdfminer.pdfcolor import PDFColorSpace
|
|
10
|
+
from pdfminer.pdfdevice import PDFDevice
|
|
11
|
+
from pdfminer.pdfdevice import PDFTextSeq
|
|
12
|
+
from pdfminer.pdffont import PDFFont
|
|
13
|
+
from pdfminer.pdfinterp import LITERAL_FORM
|
|
14
|
+
from pdfminer.pdfinterp import LITERAL_IMAGE
|
|
15
|
+
from pdfminer.pdfinterp import Color
|
|
16
|
+
from pdfminer.pdfinterp import PDFContentParser
|
|
17
|
+
from pdfminer.pdfinterp import PDFInterpreterError
|
|
18
|
+
from pdfminer.pdfinterp import PDFPageInterpreter
|
|
19
|
+
from pdfminer.pdfinterp import PDFResourceManager
|
|
20
|
+
from pdfminer.pdfinterp import PDFStackT
|
|
21
|
+
from pdfminer.pdfpage import PDFPage
|
|
22
|
+
from pdfminer.pdftypes import PDFObjRef
|
|
23
|
+
from pdfminer.pdftypes import dict_value
|
|
24
|
+
from pdfminer.pdftypes import list_value
|
|
25
|
+
from pdfminer.pdftypes import resolve1
|
|
26
|
+
from pdfminer.pdftypes import stream_value
|
|
27
|
+
from pdfminer.psexceptions import PSEOF
|
|
28
|
+
from pdfminer.psparser import PSKeyword
|
|
29
|
+
from pdfminer.psparser import keyword_name
|
|
30
|
+
from pdfminer.psparser import literal_name
|
|
31
|
+
from pdfminer.utils import MATRIX_IDENTITY
|
|
32
|
+
from pdfminer.utils import Matrix
|
|
33
|
+
from pdfminer.utils import Rect
|
|
34
|
+
from pdfminer.utils import apply_matrix_pt
|
|
35
|
+
from pdfminer.utils import mult_matrix
|
|
36
|
+
|
|
37
|
+
from babeldoc.document_il.frontend.il_creater import ILCreater
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def safe_float(o: Any) -> float | None:
|
|
43
|
+
try:
|
|
44
|
+
return float(o)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PDFPageInterpreterEx(PDFPageInterpreter):
|
|
50
|
+
"""Processor for the content of a PDF page
|
|
51
|
+
|
|
52
|
+
Reference: PDF Reference, Appendix A, Operator Summary
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
rsrcmgr: PDFResourceManager,
|
|
58
|
+
device: PDFDevice,
|
|
59
|
+
obj_patch,
|
|
60
|
+
il_creater: ILCreater,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.rsrcmgr = rsrcmgr
|
|
63
|
+
self.device = device
|
|
64
|
+
self.obj_patch = obj_patch
|
|
65
|
+
self.il_creater = il_creater
|
|
66
|
+
|
|
67
|
+
def dup(self) -> "PDFPageInterpreterEx":
|
|
68
|
+
return self.__class__(
|
|
69
|
+
self.rsrcmgr,
|
|
70
|
+
self.device,
|
|
71
|
+
self.obj_patch,
|
|
72
|
+
self.il_creater,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def init_resources(self, resources: dict[object, object]) -> None:
|
|
76
|
+
# 重载设置 fontid 和 descent
|
|
77
|
+
"""Prepare the fonts and XObjects listed in the Resource attribute."""
|
|
78
|
+
self.resources = resources
|
|
79
|
+
self.fontmap: dict[object, PDFFont] = {}
|
|
80
|
+
self.fontid: dict[PDFFont, object] = {}
|
|
81
|
+
self.xobjmap = {}
|
|
82
|
+
self.csmap: dict[str, PDFColorSpace] = PREDEFINED_COLORSPACE.copy()
|
|
83
|
+
if not resources:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
def get_colorspace(spec: object) -> PDFColorSpace | None:
|
|
87
|
+
if isinstance(spec, list):
|
|
88
|
+
name = literal_name(spec[0])
|
|
89
|
+
else:
|
|
90
|
+
name = literal_name(spec)
|
|
91
|
+
if name == "ICCBased" and isinstance(spec, list) and len(spec) >= 2:
|
|
92
|
+
return PDFColorSpace(name, stream_value(spec[1])["N"])
|
|
93
|
+
elif name == "DeviceN" and isinstance(spec, list) and len(spec) >= 2:
|
|
94
|
+
return PDFColorSpace(name, len(list_value(spec[1])))
|
|
95
|
+
else:
|
|
96
|
+
return PREDEFINED_COLORSPACE.get(name)
|
|
97
|
+
|
|
98
|
+
for k, v in dict_value(resources).items():
|
|
99
|
+
# log.debug("Resource: %r: %r", k, v)
|
|
100
|
+
if k == "Font":
|
|
101
|
+
for fontid, spec in dict_value(v).items():
|
|
102
|
+
objid = None
|
|
103
|
+
if isinstance(spec, PDFObjRef):
|
|
104
|
+
objid = spec.objid
|
|
105
|
+
spec = dict_value(spec)
|
|
106
|
+
font = self.rsrcmgr.get_font(objid, spec)
|
|
107
|
+
self.il_creater.on_page_resource_font(font, objid, fontid)
|
|
108
|
+
self.fontmap[fontid] = font
|
|
109
|
+
self.fontmap[fontid].descent = 0 # hack fix descent
|
|
110
|
+
self.fontid[self.fontmap[fontid]] = fontid
|
|
111
|
+
elif k == "ColorSpace":
|
|
112
|
+
for csid, spec in dict_value(v).items():
|
|
113
|
+
colorspace = get_colorspace(resolve1(spec))
|
|
114
|
+
if colorspace is not None:
|
|
115
|
+
self.csmap[csid] = colorspace
|
|
116
|
+
elif k == "ProcSet":
|
|
117
|
+
self.rsrcmgr.get_procset(list_value(v))
|
|
118
|
+
elif k == "XObject":
|
|
119
|
+
for xobjid, xobjstrm in dict_value(v).items():
|
|
120
|
+
self.xobjmap[xobjid] = xobjstrm
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def do_S(self) -> None:
|
|
124
|
+
# 重载过滤非公式线条
|
|
125
|
+
"""Stroke path"""
|
|
126
|
+
|
|
127
|
+
def is_black(color: Color) -> bool:
|
|
128
|
+
if isinstance(color, tuple):
|
|
129
|
+
return sum(color) == 0
|
|
130
|
+
else:
|
|
131
|
+
return color == 0
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
len(self.curpath) == 2
|
|
135
|
+
and self.curpath[0][0] == "m"
|
|
136
|
+
and self.curpath[1][0] == "l"
|
|
137
|
+
and apply_matrix_pt(self.ctm, self.curpath[0][-2:])[1]
|
|
138
|
+
== apply_matrix_pt(self.ctm, self.curpath[1][-2:])[1]
|
|
139
|
+
and is_black(self.graphicstate.scolor)
|
|
140
|
+
): # 独立直线,水平,黑色
|
|
141
|
+
# print(apply_matrix_pt(self.ctm,self.curpath[0][-2:]),apply_matrix_pt(self.ctm,self.curpath[1][-2:]),self.graphicstate.scolor)
|
|
142
|
+
self.device.paint_path(self.graphicstate, True, False, False, self.curpath)
|
|
143
|
+
self.curpath = []
|
|
144
|
+
return "n"
|
|
145
|
+
else:
|
|
146
|
+
self.curpath = []
|
|
147
|
+
|
|
148
|
+
def do_CS(self, name: PDFStackT) -> None:
|
|
149
|
+
"""Set color space for stroking operations
|
|
150
|
+
|
|
151
|
+
Introduced in PDF 1.1
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
self.il_creater.on_stroking_color_space(literal_name(name))
|
|
155
|
+
self.scs = self.csmap[literal_name(name)]
|
|
156
|
+
except KeyError:
|
|
157
|
+
if settings.STRICT:
|
|
158
|
+
raise PDFInterpreterError(f"Undefined ColorSpace: {name!r}") from None
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
def do_cs(self, name: PDFStackT) -> None:
|
|
162
|
+
"""Set color space for nonstroking operations"""
|
|
163
|
+
try:
|
|
164
|
+
self.il_creater.on_non_stroking_color_space(literal_name(name))
|
|
165
|
+
self.ncs = self.csmap[literal_name(name)]
|
|
166
|
+
except KeyError:
|
|
167
|
+
if settings.STRICT:
|
|
168
|
+
raise PDFInterpreterError(f"Undefined ColorSpace: {name!r}") from None
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
############################################################
|
|
172
|
+
# 重载过滤非公式线条(F/B)
|
|
173
|
+
def do_f(self) -> None:
|
|
174
|
+
"""Fill path using nonzero winding number rule"""
|
|
175
|
+
# self.device.paint_path(self.graphicstate, False, True, False, self.curpath)
|
|
176
|
+
self.curpath = []
|
|
177
|
+
|
|
178
|
+
def do_F(self) -> None:
|
|
179
|
+
"""Fill path using nonzero winding number rule (obsolete)"""
|
|
180
|
+
|
|
181
|
+
def do_f_a(self) -> None:
|
|
182
|
+
"""Fill path using even-odd rule"""
|
|
183
|
+
# self.device.paint_path(self.graphicstate, False, True, True, self.curpath)
|
|
184
|
+
self.curpath = []
|
|
185
|
+
|
|
186
|
+
def do_B(self) -> None:
|
|
187
|
+
"""Fill and stroke path using nonzero winding number rule"""
|
|
188
|
+
# self.device.paint_path(self.graphicstate, True, True, False, self.curpath)
|
|
189
|
+
self.curpath = []
|
|
190
|
+
|
|
191
|
+
def do_B_a(self) -> None:
|
|
192
|
+
"""Fill and stroke path using even-odd rule"""
|
|
193
|
+
# self.device.paint_path(self.graphicstate, True, True, True, self.curpath)
|
|
194
|
+
self.curpath = []
|
|
195
|
+
|
|
196
|
+
############################################################
|
|
197
|
+
# 重载返回调用参数(SCN)
|
|
198
|
+
def do_SCN(self) -> None:
|
|
199
|
+
"""Set color for stroking operations."""
|
|
200
|
+
if self.scs:
|
|
201
|
+
n = self.scs.ncomponents
|
|
202
|
+
else:
|
|
203
|
+
if settings.STRICT:
|
|
204
|
+
raise PDFInterpreterError("No colorspace specified!")
|
|
205
|
+
n = 1
|
|
206
|
+
args = self.pop(n)
|
|
207
|
+
self.il_creater.on_passthrough_per_char("SCN", args)
|
|
208
|
+
self.graphicstate.scolor = cast(Color, args)
|
|
209
|
+
return args
|
|
210
|
+
|
|
211
|
+
def do_scn(self) -> None:
|
|
212
|
+
"""Set color for nonstroking operations"""
|
|
213
|
+
if self.ncs:
|
|
214
|
+
n = self.ncs.ncomponents
|
|
215
|
+
else:
|
|
216
|
+
if settings.STRICT:
|
|
217
|
+
raise PDFInterpreterError("No colorspace specified!")
|
|
218
|
+
n = 1
|
|
219
|
+
args = self.pop(n)
|
|
220
|
+
self.il_creater.on_passthrough_per_char("scn", args)
|
|
221
|
+
self.graphicstate.ncolor = cast(Color, args)
|
|
222
|
+
return args
|
|
223
|
+
|
|
224
|
+
def do_SC(self) -> None:
|
|
225
|
+
"""Set color for stroking operations"""
|
|
226
|
+
args = self.do_SCN()
|
|
227
|
+
self.il_creater.remove_latest_passthrough_per_char_instruction()
|
|
228
|
+
self.il_creater.on_passthrough_per_char("SC", args)
|
|
229
|
+
return args
|
|
230
|
+
|
|
231
|
+
def do_sc(self) -> None:
|
|
232
|
+
"""Set color for nonstroking operations"""
|
|
233
|
+
args = self.do_scn()
|
|
234
|
+
self.il_creater.remove_latest_passthrough_per_char_instruction()
|
|
235
|
+
self.il_creater.on_passthrough_per_char("sc", args)
|
|
236
|
+
return args
|
|
237
|
+
|
|
238
|
+
def do_Do(self, xobjid_arg: PDFStackT) -> None:
|
|
239
|
+
# 重载设置 xobj 的 obj_patch
|
|
240
|
+
"""Invoke named XObject"""
|
|
241
|
+
xobjid = literal_name(xobjid_arg)
|
|
242
|
+
try:
|
|
243
|
+
xobj = stream_value(self.xobjmap[xobjid])
|
|
244
|
+
except KeyError:
|
|
245
|
+
if settings.STRICT:
|
|
246
|
+
raise PDFInterpreterError(f"Undefined xobject id: {xobjid!r}") from None
|
|
247
|
+
return
|
|
248
|
+
# log.debug("Processing xobj: %r", xobj)
|
|
249
|
+
subtype = xobj.get("Subtype")
|
|
250
|
+
if subtype is LITERAL_FORM and "BBox" in xobj:
|
|
251
|
+
interpreter = self.dup()
|
|
252
|
+
bbox = cast(Rect, list_value(xobj["BBox"]))
|
|
253
|
+
matrix = cast(Matrix, list_value(xobj.get("Matrix", MATRIX_IDENTITY)))
|
|
254
|
+
# According to PDF reference 1.7 section 4.9.1, XObjects in
|
|
255
|
+
# earlier PDFs (prior to v1.2) use the page's Resources entry
|
|
256
|
+
# instead of having their own Resources entry.
|
|
257
|
+
xobjres = xobj.get("Resources")
|
|
258
|
+
if xobjres:
|
|
259
|
+
resources = dict_value(xobjres)
|
|
260
|
+
else:
|
|
261
|
+
resources = self.resources.copy()
|
|
262
|
+
self.device.begin_figure(xobjid, bbox, matrix)
|
|
263
|
+
ctm = mult_matrix(matrix, self.ctm)
|
|
264
|
+
(x, y, x2, y2) = bbox
|
|
265
|
+
(x, y) = apply_matrix_pt(ctm, (x, y))
|
|
266
|
+
(x2, y2) = apply_matrix_pt(ctm, (x2, y2))
|
|
267
|
+
x_id = self.il_creater.on_xobj_begin((x, y, x2, y2), xobj.objid)
|
|
268
|
+
ctm_inv = np.linalg.inv(np.array(ctm[:4]).reshape(2, 2))
|
|
269
|
+
np_version = np.__version__
|
|
270
|
+
if np_version.split(".")[0] >= "2":
|
|
271
|
+
pos_inv = -np.asmatrix(ctm[4:]) * ctm_inv
|
|
272
|
+
else:
|
|
273
|
+
pos_inv = -np.mat(ctm[4:]) * ctm_inv
|
|
274
|
+
a, b, c, d = ctm_inv.reshape(4).tolist()
|
|
275
|
+
e, f = pos_inv.tolist()[0]
|
|
276
|
+
ops_base = interpreter.render_contents(
|
|
277
|
+
resources,
|
|
278
|
+
[xobj],
|
|
279
|
+
ctm=ctm,
|
|
280
|
+
)
|
|
281
|
+
self.ncs = interpreter.ncs
|
|
282
|
+
self.scs = interpreter.scs
|
|
283
|
+
self.il_creater.on_xobj_end(
|
|
284
|
+
x_id,
|
|
285
|
+
f"q {ops_base}Q {a} {b} {c} {d} {e} {f} cm ",
|
|
286
|
+
)
|
|
287
|
+
try: # 有的时候 form 字体加不上这里会烂掉
|
|
288
|
+
self.device.fontid = interpreter.fontid
|
|
289
|
+
self.device.fontmap = interpreter.fontmap
|
|
290
|
+
ops_new = self.device.end_figure(xobjid)
|
|
291
|
+
ctm_inv = np.linalg.inv(np.array(ctm[:4]).reshape(2, 2))
|
|
292
|
+
np_version = np.__version__
|
|
293
|
+
if np_version.split(".")[0] >= "2":
|
|
294
|
+
pos_inv = -np.asmatrix(ctm[4:]) * ctm_inv
|
|
295
|
+
else:
|
|
296
|
+
pos_inv = -np.mat(ctm[4:]) * ctm_inv
|
|
297
|
+
a, b, c, d = ctm_inv.reshape(4).tolist()
|
|
298
|
+
e, f = pos_inv.tolist()[0]
|
|
299
|
+
self.obj_patch[self.xobjmap[xobjid].objid] = (
|
|
300
|
+
f"q {ops_base}Q {a} {b} {c} {d} {e} {f} cm {ops_new}"
|
|
301
|
+
)
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
elif subtype is LITERAL_IMAGE and "Width" in xobj and "Height" in xobj:
|
|
305
|
+
self.device.begin_figure(xobjid, (0, 0, 1, 1), MATRIX_IDENTITY)
|
|
306
|
+
self.device.render_image(xobjid, xobj)
|
|
307
|
+
self.device.end_figure(xobjid)
|
|
308
|
+
else:
|
|
309
|
+
# unsupported xobject type.
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
def process_page(self, page: PDFPage) -> None:
|
|
313
|
+
# 重载设置 page 的 obj_patch
|
|
314
|
+
# log.debug("Processing page: %r", page)
|
|
315
|
+
# print(page.mediabox,page.cropbox)
|
|
316
|
+
# (x0, y0, x1, y1) = page.mediabox
|
|
317
|
+
(x0, y0, x1, y1) = page.cropbox
|
|
318
|
+
if page.rotate == 90:
|
|
319
|
+
ctm = (0, -1, 1, 0, -y0, x1)
|
|
320
|
+
elif page.rotate == 180:
|
|
321
|
+
ctm = (-1, 0, 0, -1, x1, y1)
|
|
322
|
+
elif page.rotate == 270:
|
|
323
|
+
ctm = (0, 1, -1, 0, y1, -x0)
|
|
324
|
+
else:
|
|
325
|
+
ctm = (1, 0, 0, 1, -x0, -y0)
|
|
326
|
+
self.il_creater.on_page_start()
|
|
327
|
+
self.il_creater.on_page_crop_box(x0, y0, x1, y1)
|
|
328
|
+
self.device.begin_page(page, ctm)
|
|
329
|
+
ops_base = self.render_contents(page.resources, page.contents, ctm=ctm)
|
|
330
|
+
self.device.fontid = self.fontid
|
|
331
|
+
self.device.fontmap = self.fontmap
|
|
332
|
+
ops_new = self.device.end_page(page)
|
|
333
|
+
# 上面渲染的时候会根据 cropbox 减掉页面偏移得到真实坐标,这里输出的时候需要用 cm 把页面偏移加回来
|
|
334
|
+
self.obj_patch[page.page_xref] = (
|
|
335
|
+
# f"q {ops_base}Q 1 0 0 1 {x0} {y0} cm {ops_new}" # ops_base 里可能有图,需要让 ops_new 里的文字覆盖在上面,使用 q/Q 重置位置矩阵
|
|
336
|
+
""
|
|
337
|
+
)
|
|
338
|
+
for obj in page.contents:
|
|
339
|
+
self.obj_patch[obj.objid] = ""
|
|
340
|
+
return ops_base
|
|
341
|
+
|
|
342
|
+
def render_contents(
|
|
343
|
+
self,
|
|
344
|
+
resources: dict[object, object],
|
|
345
|
+
streams: Sequence[object],
|
|
346
|
+
ctm: Matrix = MATRIX_IDENTITY,
|
|
347
|
+
) -> None:
|
|
348
|
+
# 重载返回指令流
|
|
349
|
+
"""Render the content streams.
|
|
350
|
+
|
|
351
|
+
This method may be called recursively.
|
|
352
|
+
"""
|
|
353
|
+
# log.debug(
|
|
354
|
+
# "render_contents: resources=%r, streams=%r, ctm=%r",
|
|
355
|
+
# resources,
|
|
356
|
+
# streams,
|
|
357
|
+
# ctm,
|
|
358
|
+
# )
|
|
359
|
+
self.init_resources(resources)
|
|
360
|
+
self.init_state(ctm)
|
|
361
|
+
return self.execute(list_value(streams))
|
|
362
|
+
|
|
363
|
+
def do_q(self) -> None:
|
|
364
|
+
"""Save graphics state"""
|
|
365
|
+
self.gstack.append(self.get_current_state())
|
|
366
|
+
self.il_creater.push_passthrough_per_char_instruction()
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
def do_Q(self) -> None:
|
|
370
|
+
"""Restore graphics state"""
|
|
371
|
+
if self.gstack:
|
|
372
|
+
self.set_current_state(self.gstack.pop())
|
|
373
|
+
self.il_creater.pop_passthrough_per_char_instruction()
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
def do_TJ(self, seq: PDFStackT) -> None:
|
|
377
|
+
"""Show text, allowing individual glyph positioning"""
|
|
378
|
+
if self.textstate.font is None:
|
|
379
|
+
if settings.STRICT:
|
|
380
|
+
raise PDFInterpreterError("No font specified!")
|
|
381
|
+
return
|
|
382
|
+
assert self.ncs is not None
|
|
383
|
+
gs = self.graphicstate.copy()
|
|
384
|
+
gs.passthrough_instruction = (
|
|
385
|
+
self.il_creater.passthrough_per_char_instruction.copy()
|
|
386
|
+
)
|
|
387
|
+
self.device.render_string(self.textstate, cast(PDFTextSeq, seq), self.ncs, gs)
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
# Run PostScript commands
|
|
391
|
+
# The Do_xxx method is the method for executing corresponding postscript instructions
|
|
392
|
+
def execute(self, streams: Sequence[object]) -> None:
|
|
393
|
+
ops = ""
|
|
394
|
+
for stream in streams:
|
|
395
|
+
self.il_creater.on_new_stream()
|
|
396
|
+
# 重载返回指令流
|
|
397
|
+
try:
|
|
398
|
+
parser = PDFContentParser([stream])
|
|
399
|
+
except PSEOF:
|
|
400
|
+
# empty page
|
|
401
|
+
return
|
|
402
|
+
while True:
|
|
403
|
+
try:
|
|
404
|
+
(_, obj) = parser.nextobject()
|
|
405
|
+
except PSEOF:
|
|
406
|
+
break
|
|
407
|
+
if isinstance(obj, PSKeyword):
|
|
408
|
+
name = keyword_name(obj)
|
|
409
|
+
act_name = (
|
|
410
|
+
name.replace("*", "_a").replace('"', "_w").replace("'", "_q")
|
|
411
|
+
)
|
|
412
|
+
method = f"do_{act_name}"
|
|
413
|
+
if hasattr(self, method):
|
|
414
|
+
func = getattr(self, method)
|
|
415
|
+
nargs = func.__code__.co_argcount - 1
|
|
416
|
+
if nargs:
|
|
417
|
+
args = self.pop(nargs)
|
|
418
|
+
# log.debug("exec: %s %r", name, args)
|
|
419
|
+
if len(args) == nargs:
|
|
420
|
+
func(*args)
|
|
421
|
+
if self.il_creater.is_passthrough_per_char_operation(
|
|
422
|
+
name,
|
|
423
|
+
):
|
|
424
|
+
self.il_creater.on_passthrough_per_char(name, args)
|
|
425
|
+
if not (
|
|
426
|
+
name[0] == "T"
|
|
427
|
+
or name
|
|
428
|
+
in ['"', "'", "EI", "MP", "DP", "BMC", "BDC"]
|
|
429
|
+
): # 过滤 T 系列文字指令,因为 EI 的参数是 obj 所以也需要过滤(只在少数文档中画横线时使用),过滤 marked 系列指令
|
|
430
|
+
p = " ".join(
|
|
431
|
+
[
|
|
432
|
+
(
|
|
433
|
+
f"{x:f}"
|
|
434
|
+
if isinstance(x, float)
|
|
435
|
+
else str(x).replace("'", "")
|
|
436
|
+
)
|
|
437
|
+
for x in args
|
|
438
|
+
],
|
|
439
|
+
)
|
|
440
|
+
ops += f"{p} {name} "
|
|
441
|
+
else:
|
|
442
|
+
# log.debug("exec: %s", name)
|
|
443
|
+
targs = func()
|
|
444
|
+
if targs is None:
|
|
445
|
+
targs = []
|
|
446
|
+
if not (name[0] == "T" or name in ["BI", "ID", "EMC"]):
|
|
447
|
+
p = " ".join(
|
|
448
|
+
[
|
|
449
|
+
(
|
|
450
|
+
f"{x:f}"
|
|
451
|
+
if isinstance(x, float)
|
|
452
|
+
else str(x).replace("'", "")
|
|
453
|
+
)
|
|
454
|
+
for x in targs
|
|
455
|
+
],
|
|
456
|
+
)
|
|
457
|
+
ops += f"{p} {name} "
|
|
458
|
+
elif settings.STRICT:
|
|
459
|
+
error_msg = f"Unknown operator: {name!r}"
|
|
460
|
+
raise PDFInterpreterError(error_msg)
|
|
461
|
+
else:
|
|
462
|
+
self.push(obj)
|
|
463
|
+
# print('REV DATA',ops)
|
|
464
|
+
return ops
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from asyncio import CancelledError
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProgressMonitor:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
translation_config,
|
|
14
|
+
stages: list[str],
|
|
15
|
+
progress_change_callback: callable = None,
|
|
16
|
+
finish_callback: callable = None,
|
|
17
|
+
report_interval: float = 0.1,
|
|
18
|
+
finish_event: asyncio.Event = None,
|
|
19
|
+
cancel_event: threading.Event = None,
|
|
20
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
21
|
+
):
|
|
22
|
+
self.stage = {k: TranslationStage(k, 0, self) for k in stages}
|
|
23
|
+
self.translation_config = translation_config
|
|
24
|
+
self.progress_change_callback = progress_change_callback
|
|
25
|
+
self.finish_callback = finish_callback
|
|
26
|
+
self.report_interval = report_interval
|
|
27
|
+
self.last_report_time = 0
|
|
28
|
+
self.finish_stage_count = 0
|
|
29
|
+
self.finish_event = finish_event
|
|
30
|
+
self.cancel_event = cancel_event
|
|
31
|
+
self.loop = loop
|
|
32
|
+
if finish_event and not loop:
|
|
33
|
+
raise ValueError("finish_event requires a loop")
|
|
34
|
+
|
|
35
|
+
def stage_start(self, stage_name: str, total: int):
|
|
36
|
+
stage = self.stage[stage_name]
|
|
37
|
+
stage.run_time += 1
|
|
38
|
+
stage.name = stage_name
|
|
39
|
+
stage.display_name = (
|
|
40
|
+
f"{stage_name} ({stage.run_time})" if stage.run_time > 1 else stage_name
|
|
41
|
+
)
|
|
42
|
+
stage.current = 0
|
|
43
|
+
stage.total = total
|
|
44
|
+
if self.progress_change_callback:
|
|
45
|
+
self.progress_change_callback(
|
|
46
|
+
type="progress_start",
|
|
47
|
+
stage=stage.display_name,
|
|
48
|
+
stage_progress=0.0,
|
|
49
|
+
stage_current=0,
|
|
50
|
+
stage_total=total,
|
|
51
|
+
)
|
|
52
|
+
return stage
|
|
53
|
+
|
|
54
|
+
def __enter__(self):
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
58
|
+
logger.debug("ProgressMonitor __exit__")
|
|
59
|
+
|
|
60
|
+
def on_finish(self):
|
|
61
|
+
if self.cancel_event:
|
|
62
|
+
self.cancel_event.set()
|
|
63
|
+
if self.finish_event and self.loop:
|
|
64
|
+
self.loop.call_soon_threadsafe(self.finish_event.set)
|
|
65
|
+
if self.cancel_event and self.cancel_event.is_set():
|
|
66
|
+
self.finish_callback(type="error", error=CancelledError)
|
|
67
|
+
|
|
68
|
+
def stage_done(self, stage):
|
|
69
|
+
self.last_report_time = 0
|
|
70
|
+
self.finish_stage_count += 1
|
|
71
|
+
if (
|
|
72
|
+
stage.current != stage.total
|
|
73
|
+
and self.cancel_event is not None
|
|
74
|
+
and not self.cancel_event.is_set()
|
|
75
|
+
):
|
|
76
|
+
logger.warning(
|
|
77
|
+
f"Stage {stage.name} completed with {stage.current}/{stage.total} items",
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
if self.progress_change_callback:
|
|
81
|
+
self.progress_change_callback(
|
|
82
|
+
type="progress_end",
|
|
83
|
+
stage=stage.display_name,
|
|
84
|
+
stage_progress=100.0,
|
|
85
|
+
stage_current=stage.total,
|
|
86
|
+
stage_total=stage.total,
|
|
87
|
+
overall_progress=self.calculate_current_progress(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def calculate_current_progress(self, stage=None):
|
|
91
|
+
progress = self.finish_stage_count * 100 / len(self.stage)
|
|
92
|
+
if stage is not None:
|
|
93
|
+
progress += stage.current * 100 / stage.total / len(self.stage)
|
|
94
|
+
return progress
|
|
95
|
+
|
|
96
|
+
def stage_update(self, stage, n: int):
|
|
97
|
+
if (
|
|
98
|
+
self.progress_change_callback
|
|
99
|
+
and time.time() - self.last_report_time > self.report_interval
|
|
100
|
+
):
|
|
101
|
+
self.progress_change_callback(
|
|
102
|
+
type="progress_update",
|
|
103
|
+
stage=stage.display_name,
|
|
104
|
+
stage_progress=stage.current * 100 / stage.total,
|
|
105
|
+
stage_current=stage.current,
|
|
106
|
+
stage_total=stage.total,
|
|
107
|
+
overall_progress=self.calculate_current_progress(stage),
|
|
108
|
+
)
|
|
109
|
+
self.last_report_time = time.time()
|
|
110
|
+
|
|
111
|
+
def translate_done(self, translate_result):
|
|
112
|
+
if self.finish_callback:
|
|
113
|
+
self.finish_callback(type="finish", translate_result=translate_result)
|
|
114
|
+
|
|
115
|
+
def translate_error(self, error):
|
|
116
|
+
if self.finish_callback:
|
|
117
|
+
self.finish_callback(type="error", error=error)
|
|
118
|
+
|
|
119
|
+
def raise_if_cancelled(self):
|
|
120
|
+
if self.cancel_event and self.cancel_event.is_set():
|
|
121
|
+
logger.info("Translation canceled")
|
|
122
|
+
raise asyncio.CancelledError
|
|
123
|
+
|
|
124
|
+
def cancel(self):
|
|
125
|
+
if self.cancel_event:
|
|
126
|
+
self.cancel_event.set()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TranslationStage:
|
|
130
|
+
def __init__(self, name: str, total: int, pm: ProgressMonitor):
|
|
131
|
+
self.name = name
|
|
132
|
+
self.display_name = name
|
|
133
|
+
self.current = 0
|
|
134
|
+
self.total = total
|
|
135
|
+
self.pm = pm
|
|
136
|
+
self.run_time = 0
|
|
137
|
+
|
|
138
|
+
def __enter__(self):
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
142
|
+
self.pm.stage_done(self)
|
|
143
|
+
|
|
144
|
+
def advance(self, n: int = 1):
|
|
145
|
+
self.current += n
|
|
146
|
+
self.pm.stage_update(self, n)
|