BabelDOC 0.3.16__tar.gz → 0.3.18__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 (102) hide show
  1. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/workflows/docs.yml +1 -1
  2. babeldoc-0.3.18/.github/workflows/lint.yml +17 -0
  3. babeldoc-0.3.18/.github/workflows/pr-lint.yml +46 -0
  4. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/workflows/publish-to-pypi.yml +1 -1
  5. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/workflows/test.yml +1 -1
  6. {babeldoc-0.3.16 → babeldoc-0.3.18}/.gitignore +1 -0
  7. {babeldoc-0.3.16 → babeldoc-0.3.18}/PKG-INFO +3 -1
  8. {babeldoc-0.3.16 → babeldoc-0.3.18}/README.md +2 -0
  9. babeldoc-0.3.18/babeldoc/__init__.py +1 -0
  10. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/const.py +1 -1
  11. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/converter.py +9 -1
  12. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/backend/pdf_creater.py +37 -22
  13. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/frontend/il_creater.py +19 -0
  14. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/il_version_1.py +18 -0
  15. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/il_version_1.rnc +4 -1
  16. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/paragraph_finder.py +49 -1
  17. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/styles_and_formulas.py +5 -2
  18. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/typesetting.py +3 -3
  19. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/utils/style_helper.py +4 -0
  20. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/docvision/rpc_doclayout.py +2 -2
  21. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/high_level.py +2 -0
  22. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/main.py +8 -1
  23. babeldoc-0.3.18/babeldoc/tools/italic_recognize_tool.py +79 -0
  24. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/translation_config.py +14 -6
  25. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/CONTRIBUTING.md +6 -0
  26. {babeldoc-0.3.16 → babeldoc-0.3.18}/pyproject.toml +4 -3
  27. {babeldoc-0.3.16 → babeldoc-0.3.18}/uv.lock +1252 -1161
  28. babeldoc-0.3.16/babeldoc/__init__.py +0 -1
  29. {babeldoc-0.3.16 → babeldoc-0.3.18}/.cursorignore +0 -0
  30. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  31. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  32. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/PULL_REQUEST_TEMPLATE/pr_form.yml +0 -0
  33. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/dependabot.yml +0 -0
  34. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/labels.yml +0 -0
  35. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/release-drafter.yml +0 -0
  36. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/workflows/codeql.yml +0 -0
  37. {babeldoc-0.3.16 → babeldoc-0.3.18}/.github/workflows/labeler.yml +0 -0
  38. {babeldoc-0.3.16 → babeldoc-0.3.18}/.pre-commit-config.yaml +0 -0
  39. {babeldoc-0.3.16 → babeldoc-0.3.18}/LICENSE +0 -0
  40. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/assets/assets.py +0 -0
  41. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/assets/embedding_assets_metadata.py +0 -0
  42. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/asynchronize/__init__.py +0 -0
  43. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/__init__.py +0 -0
  44. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
  45. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/backend/__init__.py +0 -0
  46. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/frontend/__init__.py +0 -0
  47. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/il_version_1.rng +0 -0
  48. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/il_version_1.xsd +0 -0
  49. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/__init__.py +0 -0
  50. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/add_debug_information.py +0 -0
  51. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
  52. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/il_translator.py +0 -0
  53. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/il_translator_llm_only.py +0 -0
  54. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/layout_parser.py +0 -0
  55. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/remove_descent.py +0 -0
  56. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/midend/table_parser.py +0 -0
  57. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/translator/__init__.py +0 -0
  58. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/translator/cache.py +0 -0
  59. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/translator/translator.py +0 -0
  60. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/utils/atomic_integer.py +0 -0
  61. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/utils/fontmap.py +0 -0
  62. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/utils/layout_helper.py +0 -0
  63. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/utils/priority_thread_pool_executor.py +0 -0
  64. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/document_il/xml_converter.py +0 -0
  65. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/docvision/README.md +0 -0
  66. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/docvision/__init__.py +0 -0
  67. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/docvision/doclayout.py +0 -0
  68. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/docvision/table_detection/rapidocr.py +0 -0
  69. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/pdfinterp.py +0 -0
  70. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/progress_monitor.py +0 -0
  71. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/result_merger.py +0 -0
  72. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/split_manager.py +0 -0
  73. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/tools/generate_font_metadata.py +0 -0
  74. {babeldoc-0.3.16 → babeldoc-0.3.18}/babeldoc/tools/italic_assistance.py +0 -0
  75. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/CODE_OF_CONDUCT.md +0 -0
  76. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/CONTRIBUTOR_REWARD.md +0 -0
  77. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
  78. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
  79. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
  80. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
  81. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
  82. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/README.md +0 -0
  83. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
  84. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
  85. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/README.md +0 -0
  86. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/deploy.sh +0 -0
  87. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-banner.png +0 -0
  88. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
  89. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
  90. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
  91. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
  92. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-big-logo.png +0 -0
  93. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-contributor_reward_example.png +0 -0
  94. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-preview.gif +0 -0
  95. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
  96. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
  97. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/images/babeldoc-small-logo.png +0 -0
  98. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/index.md +0 -0
  99. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/intro-to-pdf-object.md +0 -0
  100. {babeldoc-0.3.16 → babeldoc-0.3.18}/docs/requirements.txt +0 -0
  101. {babeldoc-0.3.16 → babeldoc-0.3.18}/mkdocs.yml +0 -0
  102. {babeldoc-0.3.16 → babeldoc-0.3.18}/tests/test_translation_config.py +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@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
20
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
21
21
  with:
22
22
  python-version: "3.12"
23
23
  enable-cache: true
@@ -0,0 +1,17 @@
1
+ name: Lint Code
2
+ permissions:
3
+ contents: read
4
+ pull-requests: write
5
+ on: [push]
6
+
7
+ jobs:
8
+ lint:
9
+ strategy:
10
+ fail-fast: false
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Ruff
15
+ uses: astral-sh/ruff-action@v3
16
+ - name: AutoCorrect
17
+ uses: huacnlee/autocorrect-action@main
@@ -0,0 +1,46 @@
1
+ name: Lint Code and Review Dog Report
2
+
3
+ on: [pull_request]
4
+ permissions:
5
+ contents: read
6
+ pull-requests: write
7
+ jobs:
8
+ ruff:
9
+ name: runner / ruff
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install Python
15
+ uses: actions/setup-python@v5
16
+ with:
17
+ python-version: '3.11'
18
+
19
+ - name: Install ruff
20
+ run: pip install ruff
21
+
22
+ - name: Install reviewdog
23
+ uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.2
24
+ with:
25
+ reviewdog_version: latest
26
+
27
+ - name: Run ruff with reviewdog
28
+ env:
29
+ REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30
+ run: |
31
+ ruff check . --output-format=rdjson | reviewdog -f=rdjson -reporter=github-pr-review -fail-on-error
32
+
33
+ autocorrect:
34
+ name: runner / autocorrect
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - name: AutoCorrect
39
+ uses: huacnlee/autocorrect-action@bf91ab3904c2908dd8e71312a8a83ed1eb632997 # v2.13.3
40
+ - name: Report ReviewDog
41
+ if: failure()
42
+ uses: huacnlee/autocorrect-action@bf91ab3904c2908dd8e71312a8a83ed1eb632997 # v2.13.3
43
+ env:
44
+ REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
+ with:
46
+ reviewdog: 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@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
38
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
39
39
  with:
40
40
  python-version: "3.12"
41
41
  enable-cache: true
@@ -28,7 +28,7 @@ jobs:
28
28
  path: ~/.cache/babeldoc
29
29
  key: babeldoc-assets-${{ hashFiles('babeldoc/assets/embedding_assets_metadata.py') }}
30
30
  - name: Setup uv with Python ${{ matrix.python-version }}
31
- uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
31
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
32
32
  with:
33
33
  python-version: ${{ matrix.python-version }}
34
34
  enable-cache: true
@@ -12,6 +12,7 @@ web/dist
12
12
  web/dist-ssr
13
13
  web/*.local
14
14
 
15
+ memray*
15
16
  **/*.so
16
17
  *.pdf
17
18
  *.docx
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: BabelDOC
3
- Version: 0.3.16
3
+ Version: 0.3.18
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
@@ -191,6 +191,7 @@ uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model
191
191
  - `--no-watermark`: [DEPRECATED] Use --watermark-output-mode=no_watermark instead.
192
192
  - `--translate-table-text`: Translate table text (experimental, default: False)
193
193
  - `--skip-scanned-detection`: Skip scanned document detection (default: False). When using split translation, only the first part performs detection if not skipped.
194
+ - `--ocr-workaround`: Use OCR workaround (default: False). When enabled, the tool will use OCR to detect text and fill background for scanned PDF.
194
195
 
195
196
  > [!TIP]
196
197
  > - Both `--skip-clean` and `--dual-translate-first` may help improve compatibility with some PDF readers
@@ -199,6 +200,7 @@ uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model
199
200
  > - If you encounter any compatibility issues, try using `--enhance-compatibility` first
200
201
  > - Use `--max-pages-per-part` for large documents to split them into smaller parts for translation and automatically merge them back.
201
202
  > - Use `--skip-scanned-detection` to speed up processing when you know your document is not a scanned PDF.
203
+ > - Use `--ocr-workaround` to fill background for scanned PDF. (Current assumption: background is pure white, text is pure black, this option will also auto enable `--skip-scanned-detection`)
202
204
 
203
205
  ### Translation Service Options
204
206
 
@@ -145,6 +145,7 @@ uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model
145
145
  - `--no-watermark`: [DEPRECATED] Use --watermark-output-mode=no_watermark instead.
146
146
  - `--translate-table-text`: Translate table text (experimental, default: False)
147
147
  - `--skip-scanned-detection`: Skip scanned document detection (default: False). When using split translation, only the first part performs detection if not skipped.
148
+ - `--ocr-workaround`: Use OCR workaround (default: False). When enabled, the tool will use OCR to detect text and fill background for scanned PDF.
148
149
 
149
150
  > [!TIP]
150
151
  > - Both `--skip-clean` and `--dual-translate-first` may help improve compatibility with some PDF readers
@@ -153,6 +154,7 @@ uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model
153
154
  > - If you encounter any compatibility issues, try using `--enhance-compatibility` first
154
155
  > - Use `--max-pages-per-part` for large documents to split them into smaller parts for translation and automatically merge them back.
155
156
  > - Use `--skip-scanned-detection` to speed up processing when you know your document is not a scanned PDF.
157
+ > - Use `--ocr-workaround` to fill background for scanned PDF. (Current assumption: background is pure white, text is pure black, this option will also auto enable `--skip-scanned-detection`)
156
158
 
157
159
  ### Translation Service Options
158
160
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.18"
@@ -3,7 +3,7 @@ import shutil
3
3
  import subprocess
4
4
  from pathlib import Path
5
5
 
6
- __version__ = "0.3.16"
6
+ __version__ = "0.3.18"
7
7
 
8
8
  CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"
9
9
 
@@ -92,7 +92,15 @@ class PDFConverterEx(PDFConverter):
92
92
  textwidth = font.char_width(cid)
93
93
  textdisp = font.char_disp(cid)
94
94
 
95
- font_id = self.il_creater.current_page_font_name_id_map.get(font.xobj_id, None)
95
+ if not hasattr(font, "xobj_id"):
96
+ log.debug(
97
+ f"Font {font.fontname} does not have xobj_id attribute.",
98
+ )
99
+ font_id = "UNKNOW"
100
+ else:
101
+ font_id = self.il_creater.current_page_font_name_id_map.get(
102
+ font.xobj_id, None
103
+ )
96
104
 
97
105
  item = AWLTChar(
98
106
  matrix,
@@ -31,11 +31,11 @@ def _subset_fonts_process(pdf_path, output_path):
31
31
  pdf = pymupdf.open(pdf_path)
32
32
  pdf.subset_fonts(fallback=False)
33
33
  pdf.save(output_path)
34
- # 返回0表示成功
34
+ # 返回 0 表示成功
35
35
  os._exit(0)
36
36
  except Exception as e:
37
37
  logger.error(f"Error in font subsetting subprocess: {e}")
38
- # 返回1表示失败
38
+ # 返回 1 表示失败
39
39
  os._exit(1)
40
40
 
41
41
 
@@ -69,11 +69,11 @@ def _save_pdf_clean_process(
69
69
  deflate_fonts=deflate_fonts,
70
70
  linear=linear,
71
71
  )
72
- # 返回0表示成功
72
+ # 返回 0 表示成功
73
73
  os._exit(0)
74
74
  except Exception as e:
75
75
  logger.error(f"Error in save PDF with clean=True subprocess: {e}")
76
- # 返回1表示失败
76
+ # 返回 1 表示失败
77
77
  os._exit(1)
78
78
 
79
79
 
@@ -185,21 +185,25 @@ class PDFCreater:
185
185
  except Exception:
186
186
  return set()
187
187
 
188
- def _debug_render_rectangle(
188
+ def _render_rectangle(
189
189
  self,
190
190
  draw_op: BitStream,
191
191
  rectangle: il_version_1.PdfRectangle,
192
+ line_width: float = 1,
192
193
  ):
193
- """Draw a debug rectangle in PDF for visualization purposes.
194
+ """Draw a rectangle in PDF for visualization purposes.
194
195
 
195
196
  Args:
196
197
  draw_op: BitStream to append PDF drawing operations
197
198
  rectangle: Rectangle object containing position information
199
+ line_width: Line width
198
200
  """
199
201
  x1 = rectangle.box.x
200
202
  y1 = rectangle.box.y
201
203
  x2 = rectangle.box.x2
202
204
  y2 = rectangle.box.y2
205
+ width = x2 - x1
206
+ height = y2 - y1
203
207
  # Save graphics state
204
208
  draw_op.append(b"q ")
205
209
 
@@ -207,17 +211,13 @@ class PDFCreater:
207
211
  draw_op.append(
208
212
  rectangle.graphic_state.passthrough_per_char_instruction.encode(),
209
213
  ) # Green stroke
210
- draw_op.append(b" 1 w ") # Line width
211
-
212
- # Draw four lines manually
213
- # Bottom line
214
- draw_op.append(f"{x1} {y1} m {x2} {y1} l S ".encode())
215
- # Right line
216
- draw_op.append(f"{x2} {y1} m {x2} {y2} l S ".encode())
217
- # Top line
218
- draw_op.append(f"{x2} {y2} m {x1} {y2} l S ".encode())
219
- # Left line
220
- draw_op.append(f"{x1} {y2} m {x1} {y1} l S ".encode())
214
+ if line_width > 0:
215
+ draw_op.append(f" {line_width} w ".encode()) # Line width
216
+ draw_op.append(f"{x1} {y1} {width} {height} re ".encode())
217
+ if rectangle.fill_background:
218
+ draw_op.append(b" f ")
219
+ else:
220
+ draw_op.append(b" S ")
221
221
 
222
222
  # Restore graphics state
223
223
  draw_op.append(b"Q\n")
@@ -410,7 +410,7 @@ class PDFCreater:
410
410
  for rect in page.pdf_rectangle:
411
411
  if not rect.debug_info:
412
412
  continue
413
- self._debug_render_rectangle(page_op, rect)
413
+ self._render_rectangle(page_op, rect)
414
414
  draw_op = page_op
415
415
  # Since this is a draw instruction container,
416
416
  # no additional information is needed
@@ -711,7 +711,17 @@ class PDFCreater:
711
711
  # 然后添加段落中的字符
712
712
  for paragraph in page.pdf_paragraph:
713
713
  chars.extend(self.render_paragraph_to_char(paragraph))
714
-
714
+ for rect in page.pdf_rectangle:
715
+ if (
716
+ translation_config.ocr_workaround
717
+ and not rect.debug_info
718
+ and rect.fill_background
719
+ ):
720
+ if rect.xobj_id in xobj_available_fonts:
721
+ draw_op = xobj_draw_ops[rect.xobj_id]
722
+ else:
723
+ draw_op = page_op
724
+ self._render_rectangle(draw_op, rect, line_width=0.1)
715
725
  # 渲染所有字符
716
726
  for char in chars:
717
727
  if char.char_unicode == "\n":
@@ -773,7 +783,9 @@ class PDFCreater:
773
783
  )
774
784
  # pdf.update_stream(xobj.xref_id, b'')
775
785
  for rect in page.pdf_rectangle:
776
- self._debug_render_rectangle(page_op, rect)
786
+ if translation_config.debug and rect.debug_info:
787
+ self._render_rectangle(page_op, rect)
788
+
777
789
  draw_op = page_op
778
790
  op_container = pdf.get_new_xref()
779
791
  # Since this is a draw instruction container,
@@ -783,6 +795,9 @@ class PDFCreater:
783
795
  pdf[page.page_number].set_contents(op_container)
784
796
  pbar.advance()
785
797
  translation_config.raise_if_cancelled()
798
+ gc_level = 1
799
+ if self.translation_config.ocr_workaround:
800
+ gc_level = 4
786
801
  with self.translation_config.progress_monitor.stage_start(
787
802
  SUBSET_FONT_STAGE_NAME,
788
803
  1,
@@ -811,7 +826,7 @@ class PDFCreater:
811
826
  pdf,
812
827
  mono_out_path,
813
828
  translation_config,
814
- garbage=1,
829
+ garbage=gc_level,
815
830
  deflate=True,
816
831
  clean=not translation_config.skip_clean,
817
832
  deflate_fonts=True,
@@ -864,7 +879,7 @@ class PDFCreater:
864
879
  dual,
865
880
  dual_out_path,
866
881
  translation_config,
867
- garbage=1,
882
+ garbage=gc_level,
868
883
  deflate=True,
869
884
  clean=not translation_config.skip_clean,
870
885
  deflate_fonts=True,
@@ -19,6 +19,7 @@ from pdfminer.pdftypes import resolve1 as pdftypes_resolve1
19
19
  from pdfminer.psparser import PSLiteral
20
20
 
21
21
  from babeldoc.document_il import il_version_1
22
+ from babeldoc.document_il.utils.style_helper import BLACK
22
23
  from babeldoc.document_il.utils.style_helper import YELLOW
23
24
  from babeldoc.translation_config import TranslationConfig
24
25
 
@@ -478,6 +479,7 @@ class ILCreater:
478
479
  self.current_page_font_name_id_map = {}
479
480
  self.current_page_font_char_bounding_box_map = {}
480
481
  self.mupdf_font_map: dict[int, pymupdf.Font] = {}
482
+ self.graphic_state_pool = {}
481
483
 
482
484
  def on_finish(self):
483
485
  self.progress.__exit__(None, None, None)
@@ -796,6 +798,21 @@ class ILCreater:
796
798
  f"{arg} {op}" for op, arg in gs.passthrough_instruction
797
799
  )
798
800
 
801
+ # 可能会影响部分 graphic state 准确度。不过 BabelDOC 仅使用 passthrough_per_char_instruction
802
+ # 所以应该是没啥影响
803
+ # 但是池化 graphic state 后可以减少内存占用
804
+ if (
805
+ graphic_state.passthrough_per_char_instruction
806
+ not in self.graphic_state_pool
807
+ ):
808
+ self.graphic_state_pool[graphic_state.passthrough_per_char_instruction] = (
809
+ graphic_state
810
+ )
811
+ else:
812
+ graphic_state = self.graphic_state_pool[
813
+ graphic_state.passthrough_per_char_instruction
814
+ ]
815
+
799
816
  return graphic_state
800
817
 
801
818
  def on_lt_char(self, char: LTChar):
@@ -887,6 +904,8 @@ class ILCreater:
887
904
  xobj_id=char.xobj_id,
888
905
  visual_bbox=visual_bbox,
889
906
  )
907
+ if self.translation_config.ocr_workaround:
908
+ pdf_char.pdf_style.graphic_state = BLACK
890
909
  if pdf_style.font_size == 0.0:
891
910
  logger.warning(
892
911
  "Font size is 0.0 for character %s. Skip it.",
@@ -364,6 +364,18 @@ class PdfRectangle:
364
364
  "type": "Attribute",
365
365
  },
366
366
  )
367
+ fill_background: bool | None = field(
368
+ default=None,
369
+ metadata={
370
+ "type": "Attribute",
371
+ },
372
+ )
373
+ xobj_id: int | None = field(
374
+ default=None,
375
+ metadata={
376
+ "type": "Attribute",
377
+ },
378
+ )
367
379
 
368
380
 
369
381
  @dataclass
@@ -756,6 +768,12 @@ class PdfParagraph:
756
768
  "type": "Attribute",
757
769
  },
758
770
  )
771
+ layout_id: int | None = field(
772
+ default=None,
773
+ metadata={
774
+ "type": "Attribute",
775
+ },
776
+ )
759
777
 
760
778
 
761
779
  @dataclass
@@ -115,6 +115,7 @@ PDFParagraph =
115
115
  attribute FirstLineIndent { xsd:boolean }?,
116
116
  attribute debug_id { xsd:string }?,
117
117
  attribute layout_label { xsd:string }?,
118
+ attribute layout_id { xsd:int }?,
118
119
  Box,
119
120
  PDFStyle,
120
121
  PDFParagraphComposition*
@@ -148,5 +149,7 @@ PDFRectangle =
148
149
  element pdfRectangle {
149
150
  Box,
150
151
  GraphicState,
151
- attribute debug_info { xsd:boolean }?
152
+ attribute debug_info { xsd:boolean }?,
153
+ attribute fill_background { xsd:boolean }?,
154
+ attribute xobjId { xsd:int }?
152
155
  }
@@ -8,9 +8,11 @@ from babeldoc.document_il import PdfCharacter
8
8
  from babeldoc.document_il import PdfLine
9
9
  from babeldoc.document_il import PdfParagraph
10
10
  from babeldoc.document_il import PdfParagraphComposition
11
+ from babeldoc.document_il import PdfRectangle
11
12
  from babeldoc.document_il.utils.layout_helper import Layout
12
13
  from babeldoc.document_il.utils.layout_helper import add_space_dummy_chars
13
14
  from babeldoc.document_il.utils.layout_helper import get_char_unicode_string
15
+ from babeldoc.document_il.utils.style_helper import WHITE
14
16
  from babeldoc.translation_config import TranslationConfig
15
17
 
16
18
  logger = logging.getLogger(__name__)
@@ -35,6 +37,41 @@ class ParagraphFinder:
35
37
  def __init__(self, translation_config: TranslationConfig):
36
38
  self.translation_config = translation_config
37
39
 
40
+ def add_text_fill_background(self, page: Page):
41
+ layout_map = {layout.id: layout for layout in page.page_layout}
42
+ for paragraph in page.pdf_paragraph:
43
+ layout_id = paragraph.layout_id
44
+ if layout_id is None:
45
+ continue
46
+ layout = layout_map[layout_id]
47
+ if paragraph.box is None:
48
+ continue
49
+ x1, y1, x2, y2 = (
50
+ paragraph.box.x,
51
+ paragraph.box.y,
52
+ paragraph.box.x2,
53
+ paragraph.box.y2,
54
+ )
55
+ layout_box = layout.box
56
+ if layout_box.x < x1:
57
+ x1 = layout_box.x
58
+ if layout_box.y < y1:
59
+ y1 = layout_box.y
60
+ if layout_box.x2 > x2:
61
+ x2 = layout_box.x2
62
+ if layout_box.y2 > y2:
63
+ y2 = layout_box.y2
64
+ assert x2 > x1 and y2 > y1
65
+ page.pdf_rectangle.append(
66
+ PdfRectangle(
67
+ box=Box(x1, y1, x2, y2),
68
+ fill_background=True,
69
+ graphic_state=WHITE,
70
+ debug_info=False,
71
+ xobj_id=paragraph.xobj_id,
72
+ )
73
+ )
74
+
38
75
  def update_paragraph_data(self, paragraph: PdfParagraph, update_unicode=False):
39
76
  if not paragraph.pdf_paragraph_composition:
40
77
  return
@@ -126,6 +163,12 @@ class ParagraphFinder:
126
163
  for paragraph in paragraphs:
127
164
  self.update_paragraph_data(paragraph, update_unicode=True)
128
165
 
166
+ if self.translation_config.ocr_workaround:
167
+ self.add_text_fill_background(page)
168
+ # since this is ocr file,
169
+ # image characters are not needed
170
+ page.pdf_character = []
171
+
129
172
  def is_isolated_formula(self, char: PdfCharacter):
130
173
  return char.char_unicode in (
131
174
  "(cid:122)",
@@ -176,6 +219,7 @@ class ParagraphFinder:
176
219
  if current_paragraph is None:
177
220
  current_paragraph = PdfParagraph(
178
221
  pdf_paragraph_composition=[line],
222
+ layout_id=char_layout.id,
179
223
  debug_id=generate_base58_id(),
180
224
  layout_label=char_layout.name
181
225
  if not current_layout
@@ -193,7 +237,7 @@ class ParagraphFinder:
193
237
  is_small_char = char_area < median_char_area * 0.1
194
238
 
195
239
  # 检查是否需要开始新段落
196
- # 如果字符面积小于中位数面积的10%且当前段落已有字符,则跳过新段落检测
240
+ # 如果字符面积小于中位数面积的 10% 且当前段落已有字符,则跳过新段落检测
197
241
  if not (is_small_char and current_line_chars) and (
198
242
  current_layout is None
199
243
  or char_layout.id != current_layout.id
@@ -214,6 +258,7 @@ class ParagraphFinder:
214
258
  else:
215
259
  current_paragraph = PdfParagraph(
216
260
  pdf_paragraph_composition=[line],
261
+ layout_id=current_layout.id,
217
262
  debug_id=generate_base58_id(),
218
263
  layout_label=current_layout.name,
219
264
  )
@@ -231,6 +276,7 @@ class ParagraphFinder:
231
276
  if current_paragraph is None:
232
277
  current_paragraph = PdfParagraph(
233
278
  pdf_paragraph_composition=[line],
279
+ layout_id=current_layout.id,
234
280
  debug_id=generate_base58_id(),
235
281
  layout_label=current_layout.name,
236
282
  )
@@ -420,6 +466,7 @@ class ParagraphFinder:
420
466
  unicode="",
421
467
  debug_id=generate_base58_id(),
422
468
  layout_label=paragraph.layout_label,
469
+ layout_id=paragraph.layout_id,
423
470
  )
424
471
  # 更新原段落
425
472
  paragraph.pdf_paragraph_composition = (
@@ -449,6 +496,7 @@ class ParagraphFinder:
449
496
  unicode="",
450
497
  debug_id=generate_base58_id(),
451
498
  layout_label=paragraph.layout_label,
499
+ layout_id=paragraph.layout_id,
452
500
  )
453
501
  # 更新原段落
454
502
  paragraph.pdf_paragraph_composition = (
@@ -395,7 +395,7 @@ class StylesAndFormulas:
395
395
  right_line = comp.pdf_line
396
396
  break
397
397
 
398
- # 计算与左右文本的y轴交集
398
+ # 计算与左右文本的 y 轴交集
399
399
  left_intersection = 0
400
400
  right_intersection = 0
401
401
 
@@ -413,7 +413,7 @@ class StylesAndFormulas:
413
413
  if intersection_end > intersection_start:
414
414
  right_intersection = intersection_end - intersection_start
415
415
 
416
- # 如果有两个文本段落,将交集较小的设为none
416
+ # 如果有两个文本段落,将交集较小的设为 none
417
417
  if left_line and right_line:
418
418
  if left_intersection < right_intersection:
419
419
  left_line = None
@@ -556,6 +556,9 @@ class StylesAndFormulas:
556
556
  r"|.*DJ5Exchange.*"
557
557
  r"|.*Times-Italic.*"
558
558
  r"|.*Times-BoldItalic.*"
559
+ r"|.*CronosPro.*"
560
+ r"|.*ACaslon.*"
561
+ r"|.*Frutiger.*"
559
562
  r")$"
560
563
  )
561
564
  if self.translation_config.formular_font_pattern:
@@ -221,8 +221,8 @@ class TypesettingUnit:
221
221
  # 连接号
222
222
  "~", # 全角波浪号
223
223
  "-", # 连字符减号
224
- "–", # 短破折号(EN DASH)
225
- "—", # 长破折号(EM DASH)
224
+ "–", # 短破折号 (EN DASH)
225
+ "—", # 长破折号 (EM DASH)
226
226
  # 间隔号
227
227
  "·", # 中间点
228
228
  "・", # 片假名中间点
@@ -767,7 +767,7 @@ class Typesetting:
767
767
  line_spacing = 1.7 # 初始行距为 1.7
768
768
  min_scale = 0.1 # 最小缩放因子
769
769
  min_line_spacing = 1.4 # 最小行距
770
- expand_space_flag = 0 # 0: 未扩展, 1: 已向下扩展, 2: 已向右扩展
770
+ expand_space_flag = 0 # 0: 未扩展,1: 已向下扩展,2: 已向右扩展
771
771
 
772
772
  while scale >= min_scale:
773
773
  # 尝试布局排版单元
@@ -25,6 +25,10 @@ def create_pdf_style(r, g, b, font_id="china-ss", font_size=6):
25
25
  )
26
26
 
27
27
 
28
+ BLACK = il_version_1.GraphicState(passthrough_per_char_instruction="0 g 0 G")
29
+
30
+ WHITE = il_version_1.GraphicState(passthrough_per_char_instruction="1 g 1 G")
31
+
28
32
  # Generate all color styles
29
33
  RED = il_version_1.GraphicState(
30
34
  passthrough_per_char_instruction="1.0000000000 0.2313725490 0.1882352941 rg "
@@ -43,10 +43,10 @@ def encode_image(image) -> bytes:
43
43
 
44
44
 
45
45
  @retry(
46
- stop=stop_after_attempt(3), # 最多重试3次
46
+ stop=stop_after_attempt(3), # 最多重试 3 次
47
47
  wait=wait_exponential(
48
48
  multiplier=1, min=1, max=10
49
- ), # 指数退避策略,初始1秒,最大10秒
49
+ ), # 指数退避策略,初始 1 秒,最大 10 秒
50
50
  retry=retry_if_exception_type((httpx.HTTPError, Exception)), # 针对哪些异常重试
51
51
  before_sleep=lambda retry_state: logger.warning(
52
52
  f"Request failed, retrying in {retry_state.next_action.sleep} seconds... "
@@ -586,6 +586,7 @@ def _do_translate_single(
586
586
  logger.debug(f"finish parse il from {temp_pdf_path}")
587
587
  docs = il_creater.create_il()
588
588
  logger.debug(f"finish create il from {temp_pdf_path}")
589
+ del il_creater
589
590
  if translation_config.debug:
590
591
  xml_converter.write_json(
591
592
  docs,
@@ -657,6 +658,7 @@ def _do_translate_single(
657
658
  il_translator = ILTranslator(translate_engine, translation_config)
658
659
 
659
660
  il_translator.translate(docs)
661
+ del il_translator
660
662
  logger.debug(f"finish ILTranslator from {temp_pdf_path}")
661
663
  if translation_config.debug:
662
664
  xml_converter.write_json(
@@ -23,7 +23,7 @@ from babeldoc.translation_config import TranslationConfig
23
23
  from babeldoc.translation_config import WatermarkOutputMode
24
24
 
25
25
  logger = logging.getLogger(__name__)
26
- __version__ = "0.3.16"
26
+ __version__ = "0.3.18"
27
27
 
28
28
 
29
29
  def create_parser():
@@ -210,6 +210,12 @@ def create_parser():
210
210
  default=False,
211
211
  help="Skip scanned document detection (speeds up processing for non-scanned documents)",
212
212
  )
213
+ translation_group.add_argument(
214
+ "--ocr-workaround",
215
+ action="store_true",
216
+ default=False,
217
+ help="Add text fill background (experimental)",
218
+ )
213
219
  # service option argument group
214
220
  service_group = translation_group.add_mutually_exclusive_group()
215
221
  service_group.add_argument(
@@ -377,6 +383,7 @@ async def main():
377
383
  table_model=table_model,
378
384
  show_char_box=args.show_char_box,
379
385
  skip_scanned_detection=args.skip_scanned_detection,
386
+ ocr_workaround=args.ocr_workaround,
380
387
  )
381
388
 
382
389
  # Create progress handler