BabelDOC 0.1.12__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.
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/workflows/docs.yml +1 -1
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/workflows/publish-to-pypi.yml +2 -2
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/workflows/test.yml +1 -1
- {babeldoc-0.1.12 → babeldoc-0.1.14}/PKG-INFO +3 -3
- {babeldoc-0.1.12 → babeldoc-0.1.14}/README.md +1 -2
- babeldoc-0.1.14/babeldoc/__init__.py +1 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/const.py +3 -3
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/backend/pdf_creater.py +147 -36
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/il_translator.py +2 -2
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/paragraph_finder.py +6 -6
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/styles_and_formulas.py +21 -21
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/typesetting.py +10 -6
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/translator/translator.py +2 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/utils/fontmap.py +3 -3
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/utils/layout_helper.py +16 -16
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/docvision/doclayout.py +6 -4
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/docvision/rpc_doclayout.py +1 -2
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/high_level.py +1 -5
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/main.py +115 -157
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/progress_monitor.py +6 -7
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/translation_config.py +55 -52
- {babeldoc-0.1.12 → babeldoc-0.1.14}/pyproject.toml +36 -29
- babeldoc-0.1.14/tests/test_translation_config.py +23 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/uv.lock +69 -1
- babeldoc-0.1.12/babeldoc/__init__.py +0 -1
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.cursorignore +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/dependabot.yml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/labels.yml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/release-drafter.yml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/workflows/codeql.yml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.github/workflows/labeler.yml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.gitignore +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/.pre-commit-config.yaml +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/LICENSE +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/asynchronize/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/converter.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/backend/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/frontend/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/frontend/il_creater.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.rnc +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.rng +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/il_version_1.xsd +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/add_debug_information.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/layout_parser.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/midend/remove_descent.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/translator/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/translator/cache.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/utils/style_helper.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/document_il/xml_converter.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/docvision/README.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/docvision/__init__.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/babeldoc/pdfinterp.py +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/CODE_OF_CONDUCT.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/CONTRIBUTING.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/CONTRIBUTOR_REWARD.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/README.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/README.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/deploy.sh +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/images/babeldoc-banner.png +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/images/babeldoc-contributor_reward_example.png +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/images/babeldoc-preview.gif +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/index.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/intro-to-pdf-object.md +0 -0
- {babeldoc-0.1.12 → babeldoc-0.1.14}/docs/requirements.txt +0 -0
- {babeldoc-0.1.12 → 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@
|
|
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@
|
|
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.
|
|
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@
|
|
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.
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
|
@@ -24,5 +24,5 @@ try:
|
|
|
24
24
|
.strip()
|
|
25
25
|
.decode()
|
|
26
26
|
)
|
|
27
|
-
except (OSError, FileNotFoundError):
|
|
27
|
+
except (OSError, FileNotFoundError, subprocess.CalledProcessError):
|
|
28
28
|
WATERMARK_VERSION = f"v{__version__}"
|
|
@@ -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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
584
|
+
garbage=1,
|
|
470
585
|
deflate=True,
|
|
471
586
|
clean=not translation_config.skip_clean,
|
|
472
587
|
deflate_fonts=True,
|
|
473
|
-
linear=
|
|
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
|
|
600
|
+
# Choose between alternating pages and side-by-side format
|
|
486
601
|
# Default to side-by-side if not specified
|
|
487
|
-
|
|
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
|
|
494
|
-
# Create a dual PDF with
|
|
495
|
-
dual = self.
|
|
496
|
-
|
|
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
|
|
503
|
-
dual = self.
|
|
504
|
-
|
|
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=
|
|
632
|
+
garbage=1,
|
|
522
633
|
deflate=True,
|
|
523
634
|
clean=not translation_config.skip_clean,
|
|
524
635
|
deflate_fonts=True,
|
|
525
|
-
linear=
|
|
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:
|
|
258
|
-
|
|
259
|
-
|
|
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:
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
"""按逗号拆分公式字符,返回(
|
|
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
|
-
|
|
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
|