BabelDOC 0.1.13__tar.gz → 0.1.14__tar.gz

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 (77) hide show
  1. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/workflows/docs.yml +1 -1
  2. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/workflows/publish-to-pypi.yml +2 -2
  3. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/workflows/test.yml +1 -1
  4. {babeldoc-0.1.13 → babeldoc-0.1.14}/PKG-INFO +3 -3
  5. {babeldoc-0.1.13 → babeldoc-0.1.14}/README.md +1 -2
  6. babeldoc-0.1.14/babeldoc/__init__.py +1 -0
  7. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/const.py +2 -2
  8. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/backend/pdf_creater.py +147 -36
  9. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/il_translator.py +2 -2
  10. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/paragraph_finder.py +6 -6
  11. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/styles_and_formulas.py +21 -21
  12. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/typesetting.py +10 -6
  13. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/translator/translator.py +2 -0
  14. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/utils/fontmap.py +3 -3
  15. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/utils/layout_helper.py +16 -16
  16. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/docvision/doclayout.py +6 -4
  17. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/docvision/rpc_doclayout.py +1 -2
  18. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/high_level.py +1 -5
  19. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/main.py +115 -157
  20. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/progress_monitor.py +6 -7
  21. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/translation_config.py +55 -52
  22. {babeldoc-0.1.13 → babeldoc-0.1.14}/pyproject.toml +36 -29
  23. babeldoc-0.1.14/tests/test_translation_config.py +23 -0
  24. {babeldoc-0.1.13 → babeldoc-0.1.14}/uv.lock +69 -1
  25. babeldoc-0.1.13/babeldoc/__init__.py +0 -1
  26. {babeldoc-0.1.13 → babeldoc-0.1.14}/.cursorignore +0 -0
  27. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  28. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  29. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/dependabot.yml +0 -0
  30. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/labels.yml +0 -0
  31. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/release-drafter.yml +0 -0
  32. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/workflows/codeql.yml +0 -0
  33. {babeldoc-0.1.13 → babeldoc-0.1.14}/.github/workflows/labeler.yml +0 -0
  34. {babeldoc-0.1.13 → babeldoc-0.1.14}/.gitignore +0 -0
  35. {babeldoc-0.1.13 → babeldoc-0.1.14}/.pre-commit-config.yaml +0 -0
  36. {babeldoc-0.1.13 → babeldoc-0.1.14}/LICENSE +0 -0
  37. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/asynchronize/__init__.py +0 -0
  38. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/converter.py +0 -0
  39. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/__init__.py +0 -0
  40. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/backend/__init__.py +0 -0
  41. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/frontend/__init__.py +0 -0
  42. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/frontend/il_creater.py +0 -0
  43. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.py +0 -0
  44. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.rnc +0 -0
  45. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.rng +0 -0
  46. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.xsd +0 -0
  47. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/__init__.py +0 -0
  48. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/add_debug_information.py +0 -0
  49. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/layout_parser.py +0 -0
  50. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/midend/remove_descent.py +0 -0
  51. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/translator/__init__.py +0 -0
  52. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/translator/cache.py +0 -0
  53. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/utils/style_helper.py +0 -0
  54. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/document_il/xml_converter.py +0 -0
  55. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/docvision/README.md +0 -0
  56. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/docvision/__init__.py +0 -0
  57. {babeldoc-0.1.13 → babeldoc-0.1.14}/babeldoc/pdfinterp.py +0 -0
  58. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/CODE_OF_CONDUCT.md +0 -0
  59. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/CONTRIBUTING.md +0 -0
  60. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/CONTRIBUTOR_REWARD.md +0 -0
  61. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
  62. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
  63. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
  64. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
  65. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
  66. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/README.md +0 -0
  67. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
  68. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
  69. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/README.md +0 -0
  70. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/deploy.sh +0 -0
  71. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/images/babeldoc-banner.png +0 -0
  72. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/images/babeldoc-contributor_reward_example.png +0 -0
  73. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/images/babeldoc-preview.gif +0 -0
  74. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/index.md +0 -0
  75. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/intro-to-pdf-object.md +0 -0
  76. {babeldoc-0.1.13 → babeldoc-0.1.14}/docs/requirements.txt +0 -0
  77. {babeldoc-0.1.13 → babeldoc-0.1.14}/mkdocs.yml +0 -0
@@ -17,7 +17,7 @@ jobs:
17
17
  git config user.name github-actions[bot]
18
18
  git config user.email 41898282+github-actions[bot]@users.noreply.github.com
19
19
  - name: Setup uv with Python 3.12
20
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
20
+ uses: astral-sh/setup-uv@1edb52594c857e2b5b13128931090f0640537287 # v5.3.0
21
21
  with:
22
22
  python-version: "3.12"
23
23
  enable-cache: true
@@ -35,7 +35,7 @@ jobs:
35
35
  token: ${{ secrets.GITHUB_TOKEN }}
36
36
 
37
37
  - name: Setup uv with Python 3.12
38
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
38
+ uses: astral-sh/setup-uv@1edb52594c857e2b5b13128931090f0640537287 # v5.3.0
39
39
  with:
40
40
  python-version: "3.12"
41
41
  enable-cache: true
@@ -68,7 +68,7 @@ jobs:
68
68
  run: "uv build"
69
69
 
70
70
  - name: Store the distribution packages
71
- uses: actions/upload-artifact@v4.6.0
71
+ uses: actions/upload-artifact@v4.6.1
72
72
  with:
73
73
  name: python-package-distributions
74
74
  path: dist/
@@ -28,7 +28,7 @@ jobs:
28
28
  path: ~/.cache/huggingface/hub/models--wybxc--DocLayout-YOLO-DocStructBench-onnx
29
29
  key: ${{runner.os}}-huggingmodel-wybxc--DocLayout-YOLO-DocStructBench-onnx
30
30
  - name: Setup uv with Python ${{ matrix.python-version }}
31
- uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5.2.2
31
+ uses: astral-sh/setup-uv@1edb52594c857e2b5b13128931090f0640537287 # v5.3.0
32
32
  with:
33
33
  python-version: ${{ matrix.python-version }}
34
34
  enable-cache: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: BabelDOC
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: Yet Another Document Translator
5
5
  Project-URL: Homepage, https://github.com/funstory-ai/BabelDOC
6
6
  Project-URL: Issues, https://github.com/funstory-ai/BabelDOC/issues
@@ -23,6 +23,7 @@ Requires-Dist: opencv-python>=4.10.0.84
23
23
  Requires-Dist: orjson>=3.10.14
24
24
  Requires-Dist: pdfminer-six>=20240706
25
25
  Requires-Dist: peewee>=3.17.8
26
+ Requires-Dist: pydantic>=2.10.6
26
27
  Requires-Dist: pymupdf>=1.25.1
27
28
  Requires-Dist: rich>=13.9.4
28
29
  Requires-Dist: toml>=0.10.2
@@ -142,8 +143,7 @@ uv run babeldoc --bing --files example.pdf --files example2.pdf
142
143
  - `--dual-translate-first`: Put translated pages first in dual PDF mode (default: original pages first)
143
144
  - `--disable-rich-text-translate`: Disable rich text translation (may help improve compatibility with some PDFs)
144
145
  - `--enhance-compatibility`: Enable all compatibility enhancement options (equivalent to --skip-clean --dual-translate-first --disable-rich-text-translate)
145
- - `--use-side-by-side-dual`: Use side-by-side mode for dual PDF (default). Original and translated pages are shown side by side on the same page.
146
- - `--use-alternating-pages-dual`: Use alternating pages mode for dual PDF. Original and translated pages are arranged in alternate order.
146
+ - `--use-alternating-pages-dual`: Use alternating pages mode for dual PDF. When enabled, original and translated pages are arranged in alternate order. When disabled (default), original and translated pages are shown side by side on the same page.
147
147
 
148
148
  > [!TIP]
149
149
  > - Both `--skip-clean` and `--dual-translate-first` may help improve compatibility with some PDF readers
@@ -110,8 +110,7 @@ uv run babeldoc --bing --files example.pdf --files example2.pdf
110
110
  - `--dual-translate-first`: Put translated pages first in dual PDF mode (default: original pages first)
111
111
  - `--disable-rich-text-translate`: Disable rich text translation (may help improve compatibility with some PDFs)
112
112
  - `--enhance-compatibility`: Enable all compatibility enhancement options (equivalent to --skip-clean --dual-translate-first --disable-rich-text-translate)
113
- - `--use-side-by-side-dual`: Use side-by-side mode for dual PDF (default). Original and translated pages are shown side by side on the same page.
114
- - `--use-alternating-pages-dual`: Use alternating pages mode for dual PDF. Original and translated pages are arranged in alternate order.
113
+ - `--use-alternating-pages-dual`: Use alternating pages mode for dual PDF. When enabled, original and translated pages are arranged in alternate order. When disabled (default), original and translated pages are shown side by side on the same page.
115
114
 
116
115
  > [!TIP]
117
116
  > - Both `--skip-clean` and `--dual-translate-first` may help improve compatibility with some PDF readers
@@ -0,0 +1 @@
1
+ __version__ = "0.1.14"
@@ -2,12 +2,12 @@ import shutil
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
 
5
- __version__ = "0.1.13"
5
+ __version__ = "0.1.14"
6
6
 
7
7
  CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"
8
8
 
9
9
 
10
- def get_cache_file_path(filename):
10
+ def get_cache_file_path(filename: str) -> Path:
11
11
  return CACHE_FOLDER / filename
12
12
 
13
13
 
@@ -1,5 +1,8 @@
1
1
  import logging
2
+ import os
2
3
  import re
4
+ import time
5
+ from multiprocessing import Process
3
6
  from pathlib import Path
4
7
 
5
8
  import pymupdf
@@ -16,6 +19,25 @@ SUBSET_FONT_STAGE_NAME = "Subset font"
16
19
  SAVE_PDF_STAGE_NAME = "Save PDF"
17
20
 
18
21
 
22
+ def _subset_fonts_process(pdf_path, output_path):
23
+ """Function to run in subprocess for font subsetting.
24
+
25
+ Args:
26
+ pdf_path: Path to the PDF file to subset
27
+ output_path: Path where to save the result
28
+ """
29
+ try:
30
+ pdf = pymupdf.open(pdf_path)
31
+ pdf.subset_fonts(fallback=False)
32
+ pdf.save(output_path)
33
+ # 返回0表示成功
34
+ os._exit(0)
35
+ except Exception as e:
36
+ logger.error(f"Error in font subsetting subprocess: {e}")
37
+ # 返回1表示失败
38
+ os._exit(1)
39
+
40
+
19
41
  class PDFCreater:
20
42
  stage_name = "Generate drawing instructions"
21
43
 
@@ -203,21 +225,37 @@ class PDFCreater:
203
225
  if translation_config.dual_translate_first:
204
226
  # Show translated page on left and original on right
205
227
  rect_left, rect_right = rect_right, rect_left
206
-
207
- # Show original page on left and translated on right (default)
208
- dual_page.show_pdf_page(
209
- rect_left,
210
- original_pdf,
211
- page_id,
212
- keep_proportion=True,
213
- )
214
- dual_page.show_pdf_page(
215
- rect_right,
216
- translated_pdf,
217
- page_id,
218
- keep_proportion=True,
219
- )
220
-
228
+ try:
229
+ # Show original page on left and translated on right (default)
230
+ dual_page.show_pdf_page(
231
+ rect_left,
232
+ original_pdf,
233
+ page_id,
234
+ keep_proportion=True,
235
+ )
236
+ except Exception as e:
237
+ logger.warning(
238
+ f"Failed to show original page on left and translated on right (default). "
239
+ f"Page ID: {page_id}. "
240
+ f"Original PDF: {self.original_pdf_path}. "
241
+ f"Translated PDF: {translation_config.input_file}. ",
242
+ exc_info=e,
243
+ )
244
+ try:
245
+ dual_page.show_pdf_page(
246
+ rect_right,
247
+ translated_pdf,
248
+ page_id,
249
+ keep_proportion=True,
250
+ )
251
+ except Exception as e:
252
+ logger.warning(
253
+ f"Failed to show translated page on left and original on right. "
254
+ f"Page ID: {page_id}. "
255
+ f"Original PDF: {self.original_pdf_path}. "
256
+ f"Translated PDF: {translation_config.input_file}. ",
257
+ exc_info=e,
258
+ )
221
259
  return dual
222
260
 
223
261
  def create_alternating_pages_dual_pdf(
@@ -334,7 +372,83 @@ class PDFCreater:
334
372
  # no additional information is needed
335
373
  pdf.update_stream(int(resource_xref_id), draw_op.tobytes())
336
374
  translation_config.raise_if_cancelled()
337
- pdf.subset_fonts(fallback=False)
375
+
376
+ # 使用子进程进行字体子集化
377
+ if not translation_config.skip_clean:
378
+ pdf = self.subset_fonts_in_subprocess(pdf, translation_config)
379
+ return pdf
380
+
381
+ def subset_fonts_in_subprocess(
382
+ self, pdf: pymupdf.Document, translation_config: TranslationConfig
383
+ ) -> pymupdf.Document:
384
+ """Run font subsetting in a subprocess with timeout.
385
+
386
+ Args:
387
+ pdf: The PDF document object
388
+ translation_config: Translation configuration
389
+
390
+ Returns:
391
+ Path to the PDF with subsetted fonts, or original path if subsetting failed or timed out
392
+ """
393
+ original_pdf = pdf
394
+ # Create temporary file paths
395
+ temp_input = str(
396
+ translation_config.get_working_file_path(
397
+ f"temp_subset_input_{int(time.time())}.pdf"
398
+ )
399
+ )
400
+ temp_output = str(
401
+ translation_config.get_working_file_path(
402
+ f"temp_subset_output_{int(time.time())}.pdf"
403
+ )
404
+ )
405
+
406
+ # Save PDF to temporary file without subsetting
407
+ pdf.save(temp_input)
408
+
409
+ # Create and start subprocess
410
+ process = Process(target=_subset_fonts_process, args=(temp_input, temp_output))
411
+ process.start()
412
+
413
+ # Wait for subprocess with timeout (1 minute)
414
+ timeout = 60 # 1 minutes in seconds
415
+ start_time = time.time()
416
+
417
+ while process.is_alive():
418
+ if time.time() - start_time > timeout:
419
+ logger.warning(
420
+ f"Font subsetting timeout after {timeout} seconds, terminating subprocess"
421
+ )
422
+ process.terminate()
423
+ try:
424
+ process.join(5) # Give it 5 seconds to clean up
425
+ if process.is_alive():
426
+ logger.warning("Subprocess did not terminate, killing it")
427
+ process.kill()
428
+ except Exception as e:
429
+ logger.error(f"Error terminating font subsetting process: {e}")
430
+
431
+ return original_pdf
432
+
433
+ time.sleep(0.5) # Check every half second
434
+
435
+ # Process completed, check exit code
436
+ exit_code = process.exitcode
437
+ success = exit_code == 0
438
+
439
+ # Check if subsetting was successful
440
+ if (
441
+ success
442
+ and Path(temp_output).exists()
443
+ and Path(temp_output).stat().st_size > 0
444
+ ):
445
+ logger.info("Font subsetting completed successfully")
446
+ return pymupdf.open(temp_output)
447
+ else:
448
+ logger.warning(
449
+ f"Font subsetting failed with exit code {exit_code} or produced empty file"
450
+ )
451
+ return original_pdf
338
452
 
339
453
  def write(self, translation_config: TranslationConfig) -> TranslateResult:
340
454
  basename = Path(translation_config.input_file).stem
@@ -449,7 +563,8 @@ class PDFCreater:
449
563
  1,
450
564
  ) as pbar:
451
565
  if not translation_config.skip_clean:
452
- pdf.subset_fonts(fallback=False)
566
+ pdf = self.subset_fonts_in_subprocess(pdf, translation_config)
567
+
453
568
  pbar.advance()
454
569
  with self.translation_config.progress_monitor.stage_start(
455
570
  SAVE_PDF_STAGE_NAME,
@@ -466,11 +581,11 @@ class PDFCreater:
466
581
  translation_config.raise_if_cancelled()
467
582
  pdf.save(
468
583
  mono_out_path,
469
- garbage=3,
584
+ garbage=1,
470
585
  deflate=True,
471
586
  clean=not translation_config.skip_clean,
472
587
  deflate_fonts=True,
473
- linear=True,
588
+ linear=False,
474
589
  )
475
590
  pbar.advance()
476
591
  dual_out_path = None
@@ -482,34 +597,30 @@ class PDFCreater:
482
597
  original_pdf = pymupdf.open(self.original_pdf_path)
483
598
  translated_pdf = pdf
484
599
 
485
- # Choose between side-by-side and alternating pages format
600
+ # Choose between alternating pages and side-by-side format
486
601
  # Default to side-by-side if not specified
487
- use_side_by_side = getattr(
488
- translation_config,
489
- "use_side_by_side_dual",
490
- True,
491
- )
602
+ use_alternating_pages = translation_config.use_alternating_pages_dual
492
603
 
493
- if use_side_by_side:
494
- # Create a dual PDF with side-by-side pages (original and translation)
495
- dual = self.create_side_by_side_dual_pdf(
496
- original_pdf,
604
+ if use_alternating_pages:
605
+ # Create a dual PDF with alternating pages (original and translation)
606
+ dual = self.create_alternating_pages_dual_pdf(
607
+ self.original_pdf_path,
497
608
  translated_pdf,
498
- dual_out_path,
499
609
  translation_config,
500
610
  )
501
611
  else:
502
- # Create a dual PDF with alternating pages (original and translation)
503
- dual = self.create_alternating_pages_dual_pdf(
504
- self.original_pdf_path,
612
+ # Create a dual PDF with side-by-side pages (original and translation)
613
+ dual = self.create_side_by_side_dual_pdf(
614
+ original_pdf,
505
615
  translated_pdf,
616
+ dual_out_path,
506
617
  translation_config,
507
618
  )
508
619
 
509
620
  if translation_config.debug:
510
621
  translation_config.raise_if_cancelled()
511
622
  try:
512
- self.write_debug_info(dual, translation_config)
623
+ dual = self.write_debug_info(dual, translation_config)
513
624
  except Exception:
514
625
  logger.warning(
515
626
  "Failed to write debug info to dual PDF",
@@ -518,11 +629,11 @@ class PDFCreater:
518
629
 
519
630
  dual.save(
520
631
  dual_out_path,
521
- garbage=3,
632
+ garbage=1,
522
633
  deflate=True,
523
634
  clean=not translation_config.skip_clean,
524
635
  deflate_fonts=True,
525
- linear=True,
636
+ linear=False,
526
637
  )
527
638
  if translation_config.debug:
528
639
  translation_config.raise_if_cancelled()
@@ -303,7 +303,7 @@ class ILTranslator:
303
303
  composition.pdf_same_style_characters.pdf_style,
304
304
  paragraph.pdf_style,
305
305
  )
306
- # 字号差异在0.7-1.3之间,可能是首字母变大效果,无需占位符
306
+ # 字号差异在 0.7-1.3 之间,可能是首字母变大效果,无需占位符
307
307
  or is_same_style_except_size(
308
308
  composition.pdf_same_style_characters.pdf_style,
309
309
  paragraph.pdf_style,
@@ -342,7 +342,7 @@ class ILTranslator:
342
342
  )
343
343
  return None
344
344
 
345
- # 如果占位符数量超过50,且未禁用富文本翻译,则递归调用并禁用富文本翻译
345
+ # 如果占位符数量超过 50,且未禁用富文本翻译,则递归调用并禁用富文本翻译
346
346
  if len(placeholders) > 50 and not disable_rich_text_translate:
347
347
  logger.warning(
348
348
  f"Too many placeholders ({len(placeholders)}) in paragraph[{paragraph.debug_id}], "
@@ -254,9 +254,9 @@ class ParagraphFinder:
254
254
  self,
255
255
  char: PdfCharacter,
256
256
  page: Page,
257
- xy_mode: Literal["topleft"]
258
- | Literal["bottomright"]
259
- | Literal["middle"] = "middle",
257
+ xy_mode: (
258
+ Literal["topleft"] | Literal["bottomright"] | Literal["middle"]
259
+ ) = "middle",
260
260
  ):
261
261
  tl, br, md = [
262
262
  self._get_layout(char, page, mode)
@@ -279,9 +279,9 @@ class ParagraphFinder:
279
279
  self,
280
280
  char: PdfCharacter,
281
281
  page: Page,
282
- xy_mode: Literal["topleft"]
283
- | Literal["bottomright"]
284
- | Literal["middle"] = "middle",
282
+ xy_mode: (
283
+ Literal["topleft"] | Literal["bottomright"] | Literal["middle"]
284
+ ) = "middle",
285
285
  ):
286
286
  # 这几个符号,解析出来的大小经常只有实际大小的一点点。
287
287
  # if (
@@ -59,7 +59,7 @@ class StylesAndFormulas:
59
59
  if not page.pdf_paragraph:
60
60
  return
61
61
 
62
- # 收集该页所有的公式字体ID
62
+ # 收集该页所有的公式字体 ID
63
63
  formula_font_ids = set()
64
64
  for font in page.pdf_font:
65
65
  if self.is_formulas_font(font.name):
@@ -95,7 +95,7 @@ class StylesAndFormulas:
95
95
  or char.pdf_style.font_id in formula_font_ids # 公式字体
96
96
  or char.vertical # 垂直字体
97
97
  or (
98
- # 如果是程序添加的dummy空格
98
+ # 如果是程序添加的 dummy 空格
99
99
  char.char_unicode is None and in_formula_state
100
100
  )
101
101
  )
@@ -248,7 +248,7 @@ class StylesAndFormulas:
248
248
  # 更新基准样式为所有样式的交集
249
249
  base_style = self._merge_styles(base_style, style)
250
250
 
251
- # 如果font_id或font_size为None,则使用众数
251
+ # 如果 font_id 或 font_size 为 None,则使用众数
252
252
  if base_style.font_id is None:
253
253
  base_style.font_id = self._get_mode_value([s.font_id for s in styles])
254
254
  if base_style.font_size is None:
@@ -286,7 +286,7 @@ class StylesAndFormulas:
286
286
  )
287
287
 
288
288
  def _merge_graphic_states(self, state1, state2):
289
- """合并两个GraphicState,返回它们的交集"""
289
+ """合并两个 GraphicState,返回它们的交集"""
290
290
  if state1 is None:
291
291
  return state2
292
292
  if state2 is None:
@@ -350,7 +350,7 @@ class StylesAndFormulas:
350
350
  )
351
351
 
352
352
  def process_page_offsets(self, page: Page):
353
- """计算公式的x和y偏移量"""
353
+ """计算公式的 x 和 y 偏移量"""
354
354
  if not page.pdf_paragraph:
355
355
  return
356
356
 
@@ -358,7 +358,7 @@ class StylesAndFormulas:
358
358
  if not paragraph.pdf_paragraph_composition:
359
359
  continue
360
360
 
361
- # 计算该段落的行间距,用其80%作为容差
361
+ # 计算该段落的行间距,用其 80% 作为容差
362
362
  line_spacing = self.calculate_line_spacing(paragraph)
363
363
  y_tolerance = line_spacing * 0.8
364
364
 
@@ -374,7 +374,7 @@ class StylesAndFormulas:
374
374
  for j in range(i - 1, -1, -1):
375
375
  comp = paragraph.pdf_paragraph_composition[j]
376
376
  if comp.pdf_line:
377
- # 检查y坐标是否接近,判断是否在同一行
377
+ # 检查 y 坐标是否接近,判断是否在同一行
378
378
  if abs(comp.pdf_line.box.y - formula.box.y) <= y_tolerance:
379
379
  left_line = comp.pdf_line
380
380
  break
@@ -383,22 +383,22 @@ class StylesAndFormulas:
383
383
  for j in range(i + 1, len(paragraph.pdf_paragraph_composition)):
384
384
  comp = paragraph.pdf_paragraph_composition[j]
385
385
  if comp.pdf_line:
386
- # 检查y坐标是否接近,判断是否在同一行
386
+ # 检查 y 坐标是否接近,判断是否在同一行
387
387
  if abs(comp.pdf_line.box.y - formula.box.y) <= y_tolerance:
388
388
  right_line = comp.pdf_line
389
389
  break
390
390
 
391
- # 计算x偏移量(相对于左边文本)
391
+ # 计算 x 偏移量(相对于左边文本)
392
392
  if left_line:
393
393
  formula.x_offset = formula.box.x - left_line.box.x2
394
394
  else:
395
- formula.x_offset = 0 # 如果左边没有文字,x_offset应该为0
395
+ formula.x_offset = 0 # 如果左边没有文字,x_offset 应该为 0
396
396
  if abs(formula.x_offset) < 0.1:
397
397
  formula.x_offset = 0
398
398
  if formula.x_offset > 0:
399
399
  formula.x_offset = 0
400
400
 
401
- # 计算y偏移量
401
+ # 计算 y 偏移量
402
402
  if left_line:
403
403
  # 使用底部坐标计算偏移量
404
404
  formula.y_offset = formula.box.y - left_line.box.y
@@ -415,7 +415,7 @@ class StylesAndFormulas:
415
415
  if not paragraph.pdf_paragraph_composition:
416
416
  return 0.0
417
417
 
418
- # 收集所有文本行的y坐标
418
+ # 收集所有文本行的 y 坐标
419
419
  line_y_positions = []
420
420
  for comp in paragraph.pdf_paragraph_composition:
421
421
  if comp.pdf_line:
@@ -424,7 +424,7 @@ class StylesAndFormulas:
424
424
  if len(line_y_positions) < 2:
425
425
  return 10.0 # 如果只有一行或没有行,返回一个默认值
426
426
 
427
- # 计算相邻行之间的y差值
427
+ # 计算相邻行之间的 y 差值
428
428
  line_spacings = []
429
429
  for i in range(len(line_y_positions) - 1):
430
430
  spacing = abs(line_y_positions[i] - line_y_positions[i + 1])
@@ -562,7 +562,7 @@ class StylesAndFormulas:
562
562
  # 必须包含逗号
563
563
  if "," not in text:
564
564
  return False
565
- # 检查是否包含除了数字和[]之外的其他符号
565
+ # 检查是否包含除了数字和 [] 之外的其他符号
566
566
  text_without_basic = re.sub(r"[0-9\[\],\s]", "", text)
567
567
  return bool(text_without_basic)
568
568
 
@@ -570,7 +570,7 @@ class StylesAndFormulas:
570
570
  self,
571
571
  formula: PdfFormula,
572
572
  ) -> list[tuple[list[PdfCharacter], PdfCharacter]]:
573
- """按逗号拆分公式字符,返回(字符组, 逗号字符)的列表,最后一组的逗号字符为None。
573
+ """按逗号拆分公式字符,返回 (字符组,逗号字符) 的列表,最后一组的逗号字符为 None。
574
574
  只有不在括号内的逗号才会被用作分隔符。支持的括号对包括:
575
575
  - (cid:8) 和 (cid:9)
576
576
  - ( 和 )
@@ -606,7 +606,7 @@ class StylesAndFormulas:
606
606
  """合并两个公式,保持字符的相对位置"""
607
607
  # 合并所有字符
608
608
  all_chars = formula1.pdf_character + formula2.pdf_character
609
- # 按y坐标和x坐标排序,确保字符顺序正确
609
+ # 按 y 坐标和 x 坐标排序,确保字符顺序正确
610
610
  sorted_chars = sorted(all_chars, key=lambda c: (c.box.y, c.box.x))
611
611
 
612
612
  merged_formula = PdfFormula(pdf_character=sorted_chars)
@@ -615,7 +615,7 @@ class StylesAndFormulas:
615
615
 
616
616
  def merge_overlapping_formulas(self, page: Page):
617
617
  """
618
- 合并x轴重叠且y轴有交集的相邻公式
618
+ 合并 x 轴重叠且 y 轴有交集的相邻公式
619
619
  角标可能会被识别成单独的公式,需要合并
620
620
  """
621
621
  if not page.pdf_paragraph:
@@ -638,7 +638,7 @@ class StylesAndFormulas:
638
638
  formula1 = comp1.pdf_formula
639
639
  formula2 = comp2.pdf_formula
640
640
 
641
- # 检查x轴重叠和y轴交集
641
+ # 检查 x 轴重叠和 y 轴交集
642
642
  if self.is_x_axis_contained(
643
643
  formula1.box,
644
644
  formula2.box,
@@ -650,18 +650,18 @@ class StylesAndFormulas:
650
650
  )
651
651
  # 删除第二个公式
652
652
  del paragraph.pdf_paragraph_composition[i + 1]
653
- # 不增加i,因为合并后的公式可能还需要和下一个公式合并
653
+ # 不增加 i,因为合并后的公式可能还需要和下一个公式合并
654
654
  else:
655
655
  i += 1
656
656
 
657
657
  def is_x_axis_contained(self, box1: Box, box2: Box) -> bool:
658
- """判断box1的x轴是否完全包含在box2的x轴内,或反之"""
658
+ """判断 box1 的 x 轴是否完全包含在 box2 的 x 轴内,或反之"""
659
659
  return (box1.x >= box2.x and box1.x2 <= box2.x2) or (
660
660
  box2.x >= box1.x and box2.x2 <= box1.x2
661
661
  )
662
662
 
663
663
  def has_y_intersection(self, box1: Box, box2: Box) -> bool:
664
- """判断两个box的y轴是否有交集"""
664
+ """判断两个 box 的 y 轴是否有交集"""
665
665
  return not (box1.y2 < box2.y or box2.y2 < box1.y)
666
666
 
667
667
  def process_comma_formulas(self, page: Page):
@@ -53,7 +53,11 @@ class TypesettingUnit:
53
53
  )
54
54
 
55
55
  self.font = font
56
- self.font_id = font.font_id
56
+ if font is not None:
57
+ self.font_id = font.font_id
58
+ else:
59
+ self.font_id = "base"
60
+ self.unicode = " "
57
61
  self.font_size = font_size
58
62
  self.style = style
59
63
  self.xobj_id = xobj_id
@@ -398,9 +402,9 @@ class Typesetting:
398
402
  font_size=6,
399
403
  graphic_state=il_version_1.GraphicState(),
400
404
  )
401
- text = f"本文档由funstory.ai的开源PDF翻译库BabelDOC {WATERMARK_VERSION} (http://yadt.io)翻译,本仓库正在积极的建设当中,欢迎star和关注。"
405
+ text = f"本文档由 funstory.ai 的开源 PDF 翻译库 BabelDOC {WATERMARK_VERSION} (http://yadt.io) 翻译,本仓库正在积极的建设当中,欢迎 star 和关注。"
402
406
  if self.translation_config.debug:
403
- text += "\n 当前为DEBUG模式,将显示更多辅助信息。请注意,部分框的位置对应原文,但在译文中可能不正确。"
407
+ text += "\n 当前为 DEBUG 模式,将显示更多辅助信息。请注意,部分框的位置对应原文,但在译文中可能不正确。"
404
408
  page.pdf_paragraph.append(
405
409
  il_version_1.PdfParagraph(
406
410
  first_line_indent=False,
@@ -463,7 +467,7 @@ class Typesetting:
463
467
  line_spacing: 行间距
464
468
 
465
469
  Returns:
466
- tuple[list[TypesettingUnit], bool]: (已布局的排版单元列表, 是否所有单元都放得下)
470
+ tuple[list[TypesettingUnit], bool]: (已布局的排版单元列表,是否所有单元都放得下)
467
471
  """
468
472
  # 计算字号众数
469
473
  font_sizes = []
@@ -547,7 +551,7 @@ class Typesetting:
547
551
  relocated_unit = unit.relocate(current_x, current_y, scale)
548
552
  typeset_units.append(relocated_unit)
549
553
 
550
- # workaround: 超长行距暂时没找到具体原因,有待进一步修复。这里的1.2是魔法数字!
554
+ # workaround: 超长行距暂时没找到具体原因,有待进一步修复。这里的 1.2 是魔法数字!
551
555
  # 更新当前行的最大高度
552
556
  if line_height == 0 or line_height * 1.2 > unit_height > line_height:
553
557
  line_height = unit_height
@@ -567,7 +571,7 @@ class Typesetting:
567
571
  ):
568
572
  box = paragraph.box
569
573
  scale = 1.0
570
- line_spacing = 1.5 # 初始行距为1.7
574
+ line_spacing = 1.5 # 初始行距为 1.7
571
575
  min_scale = 0.1 # 最小缩放因子
572
576
  min_line_spacing = 1.4 # 最小行距
573
577
  expand_space_flag = False
@@ -5,6 +5,7 @@ import threading
5
5
  import time
6
6
  import unicodedata
7
7
  from abc import ABC
8
+ from abc import abstractmethod
8
9
 
9
10
  import openai
10
11
  import requests
@@ -115,6 +116,7 @@ class BaseTranslator(ABC):
115
116
  self.cache.set(text, translation)
116
117
  return translation
117
118
 
119
+ @abstractmethod
118
120
  def do_translate(self, text):
119
121
  """
120
122
  Actual translate text, override this method
@@ -117,7 +117,7 @@ class FontMapper:
117
117
  continue
118
118
  if bold != font.is_bold:
119
119
  continue
120
- # 不知道什么原因,思源黑体的 serif 属性为1,先workaround
120
+ # 不知道什么原因,思源黑体的 serif 属性为 1,先 workaround
121
121
  if serif == 1 and "serif" not in font.font_id:
122
122
  continue
123
123
  if serif == 0 and "serif" in font.font_id:
@@ -129,7 +129,7 @@ class FontMapper:
129
129
  if self.fallback_font.has_glyph(current_char):
130
130
  return self.fallback_font
131
131
 
132
- logger.error(
132
+ logger.warning(
133
133
  f"Can't find font for {char_unicode}({current_char}). "
134
134
  f"Original font: {original_font}. "
135
135
  f"Char unicode: {char_unicode}. ",
@@ -219,7 +219,7 @@ class FontMapper:
219
219
  )
220
220
  pbar.advance(1)
221
221
 
222
- # 批量添加字体到页面和XObject
222
+ # 批量添加字体到页面和 XObject
223
223
  for page in il.page:
224
224
  page.pdf_font.extend(pdf_fonts)
225
225
  for xobj in page.pdf_xobject: