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.
Files changed (42) hide show
  1. babeldoc/__init__.py +1 -0
  2. babeldoc/asynchronize/__init__.py +51 -0
  3. babeldoc/const.py +7 -0
  4. babeldoc/converter.py +510 -0
  5. babeldoc/document_il/__init__.py +43 -0
  6. babeldoc/document_il/backend/__init__.py +0 -0
  7. babeldoc/document_il/backend/pdf_creater.py +402 -0
  8. babeldoc/document_il/frontend/__init__.py +0 -0
  9. babeldoc/document_il/frontend/il_creater.py +348 -0
  10. babeldoc/document_il/il_version_1.py +799 -0
  11. babeldoc/document_il/il_version_1.rnc +141 -0
  12. babeldoc/document_il/il_version_1.rng +390 -0
  13. babeldoc/document_il/il_version_1.xsd +235 -0
  14. babeldoc/document_il/midend/__init__.py +0 -0
  15. babeldoc/document_il/midend/add_debug_information.py +138 -0
  16. babeldoc/document_il/midend/il_translator.py +553 -0
  17. babeldoc/document_il/midend/layout_parser.py +180 -0
  18. babeldoc/document_il/midend/paragraph_finder.py +452 -0
  19. babeldoc/document_il/midend/remove_descent.py +168 -0
  20. babeldoc/document_il/midend/styles_and_formulas.py +701 -0
  21. babeldoc/document_il/midend/typesetting.py +761 -0
  22. babeldoc/document_il/translator/__init__.py +0 -0
  23. babeldoc/document_il/translator/cache.py +145 -0
  24. babeldoc/document_il/translator/translator.py +267 -0
  25. babeldoc/document_il/utils/fontmap.py +227 -0
  26. babeldoc/document_il/utils/layout_helper.py +467 -0
  27. babeldoc/document_il/utils/style_helper.py +86 -0
  28. babeldoc/document_il/xml_converter.py +51 -0
  29. babeldoc/docvision/README.md +0 -0
  30. babeldoc/docvision/__init__.py +0 -0
  31. babeldoc/docvision/doclayout.py +278 -0
  32. babeldoc/docvision/rpc_doclayout.py +162 -0
  33. babeldoc/high_level.py +468 -0
  34. babeldoc/main.py +466 -0
  35. babeldoc/pdfinterp.py +464 -0
  36. babeldoc/progress_monitor.py +146 -0
  37. babeldoc/translation_config.py +156 -0
  38. babeldoc-0.1.6.dist-info/METADATA +283 -0
  39. babeldoc-0.1.6.dist-info/RECORD +42 -0
  40. babeldoc-0.1.6.dist-info/WHEEL +4 -0
  41. babeldoc-0.1.6.dist-info/entry_points.txt +2 -0
  42. 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)