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
@@ -0,0 +1,553 @@
1
+ import concurrent.futures
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from tqdm import tqdm
7
+
8
+ from babeldoc.document_il import Document
9
+ from babeldoc.document_il import Page
10
+ from babeldoc.document_il import PdfFont
11
+ from babeldoc.document_il import PdfFormula
12
+ from babeldoc.document_il import PdfParagraph
13
+ from babeldoc.document_il import PdfParagraphComposition
14
+ from babeldoc.document_il import PdfSameStyleCharacters
15
+ from babeldoc.document_il import PdfSameStyleUnicodeCharacters
16
+ from babeldoc.document_il import PdfStyle
17
+ from babeldoc.document_il.translator.translator import BaseTranslator
18
+ from babeldoc.document_il.utils.fontmap import FontMapper
19
+ from babeldoc.document_il.utils.layout_helper import get_char_unicode_string
20
+ from babeldoc.document_il.utils.layout_helper import is_same_style
21
+ from babeldoc.document_il.utils.layout_helper import is_same_style_except_font
22
+ from babeldoc.document_il.utils.layout_helper import is_same_style_except_size
23
+ from babeldoc.translation_config import TranslationConfig
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class RichTextPlaceholder:
29
+ def __init__(
30
+ self,
31
+ placeholder_id: int,
32
+ composition: PdfSameStyleCharacters,
33
+ left_placeholder: str,
34
+ right_placeholder: str,
35
+ ):
36
+ self.id = placeholder_id
37
+ self.composition = composition
38
+ self.left_placeholder = left_placeholder
39
+ self.right_placeholder = right_placeholder
40
+
41
+
42
+ class FormulaPlaceholder:
43
+ def __init__(self, placeholder_id: int, formula: PdfFormula, placeholder: str):
44
+ self.id = placeholder_id
45
+ self.formula = formula
46
+ self.placeholder = placeholder
47
+
48
+
49
+ class PbarContext:
50
+ def __init__(self, pbar):
51
+ self.pbar = pbar
52
+
53
+ def __enter__(self):
54
+ return self.pbar
55
+
56
+ def __exit__(self, exc_type, exc_value, traceback):
57
+ self.pbar.advance()
58
+
59
+
60
+ class DocumentTranslateTracker:
61
+ def __init__(self):
62
+ self.page = []
63
+
64
+ def new_page(self):
65
+ page = PageTranslateTracker()
66
+ self.page.append(page)
67
+ return page
68
+
69
+ def to_json(self):
70
+ pages = []
71
+ for page in self.page:
72
+ paragraphs = []
73
+ for para in page.paragraph:
74
+ i_str = getattr(para, "input", None)
75
+ o_str = getattr(para, "output", None)
76
+ pdf_unicode = getattr(para, "pdf_unicode", None)
77
+ if pdf_unicode is None or i_str is None:
78
+ continue
79
+ paragraphs.append(
80
+ {
81
+ "input": i_str,
82
+ "output": o_str,
83
+ "pdf_unicode": pdf_unicode,
84
+ },
85
+ )
86
+ pages.append({"paragraph": paragraphs})
87
+ return json.dumps({"page": pages}, ensure_ascii=False, indent=2)
88
+
89
+
90
+ class PageTranslateTracker:
91
+ def __init__(self):
92
+ self.paragraph = []
93
+
94
+ def new_paragraph(self):
95
+ paragraph = ParagraphTranslateTracker()
96
+ self.paragraph.append(paragraph)
97
+ return paragraph
98
+
99
+
100
+ class ParagraphTranslateTracker:
101
+ def __init__(self):
102
+ pass
103
+
104
+ def set_pdf_unicode(self, unicode: str):
105
+ self.pdf_unicode = unicode
106
+
107
+ def set_input(self, input_text: str):
108
+ self.input = input_text
109
+
110
+ def set_output(self, output: str):
111
+ self.output = output
112
+
113
+
114
+ class ILTranslator:
115
+ stage_name = "翻译段落"
116
+
117
+ def __init__(
118
+ self,
119
+ translate_engine: BaseTranslator,
120
+ translation_config: TranslationConfig,
121
+ ):
122
+ self.translate_engine = translate_engine
123
+ self.translation_config = translation_config
124
+ self.font_mapper = FontMapper(translation_config)
125
+
126
+ def translate(self, docs: Document):
127
+ tracker = DocumentTranslateTracker()
128
+ # count total paragraph
129
+ total = sum(len(page.pdf_paragraph) for page in docs.page)
130
+ with self.translation_config.progress_monitor.stage_start(
131
+ self.stage_name,
132
+ total,
133
+ ) as pbar:
134
+ with concurrent.futures.ThreadPoolExecutor(
135
+ max_workers=min(
136
+ self.translation_config.qps * 2,
137
+ self.translation_config.qps + 5,
138
+ ),
139
+ ) as executor:
140
+ for page in docs.page:
141
+ self.process_page(page, executor, pbar, tracker.new_page())
142
+
143
+ path = self.translation_config.get_working_file_path("translate_tracking.json")
144
+
145
+ if self.translation_config.debug:
146
+ logger.debug(f"save translate tracking to {path}")
147
+ with Path(path).open("w", encoding="utf-8") as f:
148
+ f.write(tracker.to_json())
149
+
150
+ def process_page(
151
+ self,
152
+ page: Page,
153
+ executor: concurrent.futures.ThreadPoolExecutor,
154
+ pbar: tqdm | None = None,
155
+ tracker: PageTranslateTracker = None,
156
+ ):
157
+ self.translation_config.raise_if_cancelled()
158
+ for paragraph in page.pdf_paragraph:
159
+ page_font_map = {}
160
+ for font in page.pdf_font:
161
+ page_font_map[font.font_id] = font
162
+ page_xobj_font_map = {}
163
+ for xobj in page.pdf_xobject:
164
+ page_xobj_font_map[xobj.xobj_id] = page_font_map.copy()
165
+ for font in xobj.pdf_font:
166
+ page_xobj_font_map[xobj.xobj_id][font.font_id] = font
167
+ # self.translate_paragraph(paragraph, pbar,tracker.new_paragraph(), page_font_map, page_xobj_font_map)
168
+ executor.submit(
169
+ self.translate_paragraph,
170
+ paragraph,
171
+ pbar,
172
+ tracker.new_paragraph(),
173
+ page_font_map,
174
+ page_xobj_font_map,
175
+ )
176
+
177
+ class TranslateInput:
178
+ def __init__(
179
+ self,
180
+ unicode: str,
181
+ placeholders: [RichTextPlaceholder | FormulaPlaceholder],
182
+ base_style: PdfStyle = None,
183
+ ):
184
+ self.unicode = unicode
185
+ self.placeholders = placeholders
186
+ self.base_style = base_style
187
+
188
+ def create_formula_placeholder(
189
+ self,
190
+ formula: PdfFormula,
191
+ formula_id: int,
192
+ paragraph: PdfParagraph,
193
+ ):
194
+ placeholder = self.translate_engine.get_formular_placeholder(formula_id)
195
+ if placeholder in paragraph.unicode:
196
+ return self.create_formula_placeholder(formula, formula_id + 1, paragraph)
197
+
198
+ return FormulaPlaceholder(formula_id, formula, placeholder)
199
+
200
+ def create_rich_text_placeholder(
201
+ self,
202
+ composition: PdfSameStyleCharacters,
203
+ composition_id: int,
204
+ paragraph: PdfParagraph,
205
+ ):
206
+ left_placeholder = self.translate_engine.get_rich_text_left_placeholder(
207
+ composition_id,
208
+ )
209
+ right_placeholder = self.translate_engine.get_rich_text_right_placeholder(
210
+ composition_id,
211
+ )
212
+ if (
213
+ left_placeholder in paragraph.unicode
214
+ or right_placeholder in paragraph.unicode
215
+ ):
216
+ return self.create_rich_text_placeholder(
217
+ composition,
218
+ composition_id + 1,
219
+ paragraph,
220
+ )
221
+
222
+ return RichTextPlaceholder(
223
+ composition_id,
224
+ composition,
225
+ left_placeholder,
226
+ right_placeholder,
227
+ )
228
+
229
+ def get_translate_input(
230
+ self,
231
+ paragraph: PdfParagraph,
232
+ page_font_map: dict[str, PdfFont] = None,
233
+ ):
234
+ if not paragraph.pdf_paragraph_composition:
235
+ return
236
+ if len(paragraph.pdf_paragraph_composition) == 1:
237
+ # 如果整个段落只有一个组成部分,那么直接返回,不需要套占位符等
238
+ composition = paragraph.pdf_paragraph_composition[0]
239
+ if (
240
+ composition.pdf_line
241
+ or composition.pdf_same_style_characters
242
+ or composition.pdf_character
243
+ ):
244
+ return self.TranslateInput(paragraph.unicode, [], paragraph.pdf_style)
245
+ elif composition.pdf_formula:
246
+ # 不需要翻译纯公式
247
+ return None
248
+ elif composition.pdf_same_style_unicode_characters:
249
+ # DEBUG INSERT CHAR, NOT TRANSLATE
250
+ return None
251
+ else:
252
+ logger.error(
253
+ f"Unknown composition type. "
254
+ f"Composition: {composition}. "
255
+ f"Paragraph: {paragraph}. ",
256
+ )
257
+ return None
258
+
259
+ placeholder_id = 1
260
+ placeholders = []
261
+ chars = []
262
+ for composition in paragraph.pdf_paragraph_composition:
263
+ if composition.pdf_line:
264
+ chars.extend(composition.pdf_line.pdf_character)
265
+ elif composition.pdf_formula:
266
+ formula_placeholder = self.create_formula_placeholder(
267
+ composition.pdf_formula,
268
+ placeholder_id,
269
+ paragraph,
270
+ )
271
+ placeholders.append(formula_placeholder)
272
+ # 公式只需要一个占位符,所以 id+1
273
+ placeholder_id = formula_placeholder.id + 1
274
+ chars.extend(formula_placeholder.placeholder)
275
+ elif composition.pdf_character:
276
+ chars.append(composition.pdf_character)
277
+ elif composition.pdf_same_style_characters:
278
+ fonta = self.font_mapper.map(
279
+ page_font_map[
280
+ composition.pdf_same_style_characters.pdf_style.font_id
281
+ ],
282
+ "1",
283
+ )
284
+ fontb = self.font_mapper.map(
285
+ page_font_map[paragraph.pdf_style.font_id],
286
+ "1",
287
+ )
288
+ if (
289
+ # 样式和段落基准样式一致,无需占位符
290
+ is_same_style(
291
+ composition.pdf_same_style_characters.pdf_style,
292
+ paragraph.pdf_style,
293
+ )
294
+ # 字号差异在0.7-1.3之间,可能是首字母变大效果,无需占位符
295
+ or is_same_style_except_size(
296
+ composition.pdf_same_style_characters.pdf_style,
297
+ paragraph.pdf_style,
298
+ )
299
+ or (
300
+ # 除了字体以外样式都和基准一样,并且字体都映射到同一个字体。无需占位符
301
+ is_same_style_except_font(
302
+ composition.pdf_same_style_characters.pdf_style,
303
+ paragraph.pdf_style,
304
+ )
305
+ and fonta
306
+ and fontb
307
+ and fonta.font_id == fontb.font_id
308
+ )
309
+ # or len(composition.pdf_same_style_characters.pdf_character) == 1
310
+ ):
311
+ chars.extend(composition.pdf_same_style_characters.pdf_character)
312
+ continue
313
+ placeholder = self.create_rich_text_placeholder(
314
+ composition.pdf_same_style_characters,
315
+ placeholder_id,
316
+ paragraph,
317
+ )
318
+ placeholders.append(placeholder)
319
+ # 样式需要一左一右两个占位符,所以 id+2
320
+ placeholder_id = placeholder.id + 2
321
+ chars.append(placeholder.left_placeholder)
322
+ chars.extend(composition.pdf_same_style_characters.pdf_character)
323
+ chars.append(placeholder.right_placeholder)
324
+ else:
325
+ logger.error(
326
+ "Unexpected PdfParagraphComposition type "
327
+ "in PdfParagraph during translation. "
328
+ f"Composition: {composition}. "
329
+ f"Paragraph: {paragraph}. ",
330
+ )
331
+ return None
332
+
333
+ text = get_char_unicode_string(chars)
334
+ return self.TranslateInput(text, placeholders, paragraph.pdf_style)
335
+
336
+ def process_formula(
337
+ self,
338
+ formula: PdfFormula,
339
+ formula_id: int,
340
+ paragraph: PdfParagraph,
341
+ ):
342
+ placeholder = self.create_formula_placeholder(formula, formula_id, paragraph)
343
+ if placeholder.placeholder in paragraph.unicode:
344
+ return self.process_formula(formula, formula_id + 1, paragraph)
345
+
346
+ return placeholder
347
+
348
+ def process_composition(
349
+ self,
350
+ composition: PdfSameStyleCharacters,
351
+ composition_id: int,
352
+ paragraph: PdfParagraph,
353
+ ):
354
+ placeholder = self.create_rich_text_placeholder(
355
+ composition,
356
+ composition_id,
357
+ paragraph,
358
+ )
359
+ if (
360
+ placeholder.left_placeholder in paragraph.unicode
361
+ or placeholder.right_placeholder in paragraph.unicode
362
+ ):
363
+ return self.process_composition(
364
+ composition,
365
+ composition_id + 1,
366
+ paragraph,
367
+ )
368
+
369
+ return placeholder
370
+
371
+ def parse_translate_output(
372
+ self,
373
+ input_text: TranslateInput,
374
+ output: str,
375
+ ) -> [PdfParagraphComposition]:
376
+ import re
377
+
378
+ result = []
379
+
380
+ # 如果没有占位符,直接返回整个文本
381
+ if not input_text.placeholders:
382
+ comp = PdfParagraphComposition()
383
+ comp.pdf_same_style_unicode_characters = PdfSameStyleUnicodeCharacters()
384
+ comp.pdf_same_style_unicode_characters.unicode = output
385
+ comp.pdf_same_style_unicode_characters.pdf_style = input_text.base_style
386
+ return [comp]
387
+
388
+ # 构建正则表达式模式
389
+ patterns = []
390
+ placeholder_patterns = []
391
+ placeholder_map = {}
392
+
393
+ for placeholder in input_text.placeholders:
394
+ if isinstance(placeholder, FormulaPlaceholder):
395
+ # 转义特殊字符
396
+ pattern = re.escape(placeholder.placeholder)
397
+ patterns.append(f"({pattern})")
398
+ placeholder_patterns.append(f"({pattern})")
399
+ placeholder_map[placeholder.placeholder] = placeholder
400
+ else:
401
+ left = re.escape(placeholder.left_placeholder)
402
+ right = re.escape(placeholder.right_placeholder)
403
+ patterns.append(f"({left}.*?{right})")
404
+ placeholder_patterns.append(f"({left})")
405
+ placeholder_patterns.append(f"({right})")
406
+ placeholder_map[placeholder.left_placeholder] = placeholder
407
+
408
+ # 合并所有模式
409
+ combined_pattern = "|".join(patterns)
410
+ combined_placeholder_pattern = "|".join(placeholder_patterns)
411
+
412
+ def remove_placeholder(text: str):
413
+ return re.sub(combined_placeholder_pattern, "", text)
414
+
415
+ # 找到所有匹配
416
+ last_end = 0
417
+ for match in re.finditer(combined_pattern, output):
418
+ # 处理匹配之前的普通文本
419
+ if match.start() > last_end:
420
+ text = output[last_end : match.start()]
421
+ if text:
422
+ comp = PdfParagraphComposition()
423
+ comp.pdf_same_style_unicode_characters = (
424
+ PdfSameStyleUnicodeCharacters()
425
+ )
426
+ comp.pdf_same_style_unicode_characters.unicode = remove_placeholder(
427
+ text,
428
+ )
429
+ comp.pdf_same_style_unicode_characters.pdf_style = (
430
+ input_text.base_style
431
+ )
432
+ result.append(comp)
433
+
434
+ matched_text = match.group(0)
435
+
436
+ # 处理占位符
437
+ if any(
438
+ isinstance(p, FormulaPlaceholder) and matched_text == p.placeholder
439
+ for p in input_text.placeholders
440
+ ):
441
+ # 处理公式占位符
442
+ placeholder = next(
443
+ p
444
+ for p in input_text.placeholders
445
+ if isinstance(p, FormulaPlaceholder)
446
+ and matched_text == p.placeholder
447
+ )
448
+ comp = PdfParagraphComposition()
449
+ comp.pdf_formula = placeholder.formula
450
+ result.append(comp)
451
+ else:
452
+ # 处理富文本占位符
453
+ placeholder = next(
454
+ p
455
+ for p in input_text.placeholders
456
+ if not isinstance(p, FormulaPlaceholder)
457
+ and matched_text.startswith(p.left_placeholder)
458
+ )
459
+ text = matched_text[
460
+ len(placeholder.left_placeholder) : -len(
461
+ placeholder.right_placeholder,
462
+ )
463
+ ]
464
+
465
+ if isinstance(
466
+ placeholder.composition,
467
+ PdfSameStyleCharacters,
468
+ ) and text.replace(" ", "") == "".join(
469
+ x.char_unicode for x in placeholder.composition.pdf_character
470
+ ).replace(" ", ""):
471
+ comp = PdfParagraphComposition(
472
+ pdf_same_style_characters=placeholder.composition,
473
+ )
474
+ else:
475
+ comp = PdfParagraphComposition()
476
+ comp.pdf_same_style_unicode_characters = (
477
+ PdfSameStyleUnicodeCharacters()
478
+ )
479
+ comp.pdf_same_style_unicode_characters.pdf_style = (
480
+ placeholder.composition.pdf_style
481
+ )
482
+ comp.pdf_same_style_unicode_characters.unicode = remove_placeholder(
483
+ text,
484
+ )
485
+ result.append(comp)
486
+
487
+ last_end = match.end()
488
+
489
+ # 处理最后的普通文本
490
+ if last_end < len(output):
491
+ text = output[last_end:]
492
+ if text:
493
+ comp = PdfParagraphComposition()
494
+ comp.pdf_same_style_unicode_characters = PdfSameStyleUnicodeCharacters()
495
+ comp.pdf_same_style_unicode_characters.unicode = remove_placeholder(
496
+ text,
497
+ )
498
+ comp.pdf_same_style_unicode_characters.pdf_style = input_text.base_style
499
+ result.append(comp)
500
+
501
+ return result
502
+
503
+ def translate_paragraph(
504
+ self,
505
+ paragraph: PdfParagraph,
506
+ pbar: tqdm | None = None,
507
+ tracker: ParagraphTranslateTracker = None,
508
+ page_font_map: dict[str, PdfFont] = None,
509
+ xobj_font_map: dict[int, dict[str, PdfFont]] = None,
510
+ ):
511
+ self.translation_config.raise_if_cancelled()
512
+ with PbarContext(pbar):
513
+ try:
514
+ if paragraph.vertical:
515
+ return
516
+
517
+ tracker.set_pdf_unicode(paragraph.unicode)
518
+ if paragraph.xobj_id in xobj_font_map:
519
+ page_font_map = xobj_font_map[paragraph.xobj_id]
520
+ translate_input = self.get_translate_input(paragraph, page_font_map)
521
+ if not translate_input:
522
+ return
523
+
524
+ tracker.set_input(translate_input.unicode)
525
+
526
+ text = translate_input.unicode
527
+ translated_text = self.translate_engine.translate(text)
528
+
529
+ tracker.set_output(translated_text)
530
+
531
+ if translated_text == text:
532
+ return
533
+
534
+ paragraph.unicode = translated_text
535
+ paragraph.pdf_paragraph_composition = self.parse_translate_output(
536
+ translate_input,
537
+ translated_text,
538
+ )
539
+ for composition in paragraph.pdf_paragraph_composition:
540
+ if (
541
+ composition.pdf_same_style_unicode_characters
542
+ and composition.pdf_same_style_unicode_characters.pdf_style
543
+ is None
544
+ ):
545
+ composition.pdf_same_style_unicode_characters.pdf_style = (
546
+ paragraph.pdf_style
547
+ )
548
+ except Exception as e:
549
+ logger.exception(
550
+ f"Error translating paragraph. Paragraph: {paragraph}. Error: {e}. ",
551
+ )
552
+ # ignore error and continue
553
+ return