BabelDOC 0.1.32__tar.gz → 0.2.0__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.32 → babeldoc-0.2.0}/.github/workflows/test.yml +5 -1
- {babeldoc-0.1.32 → babeldoc-0.2.0}/LICENSE +1 -1
- {babeldoc-0.1.32 → babeldoc-0.2.0}/PKG-INFO +5 -6
- {babeldoc-0.1.32 → babeldoc-0.2.0}/README.md +3 -5
- babeldoc-0.2.0/babeldoc/__init__.py +1 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/const.py +1 -1
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/__init__.py +4 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/frontend/il_creater.py +225 -4
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.py +142 -73
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.rnc +12 -2
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.rng +27 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.xsd +20 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/il_translator.py +8 -8
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/paragraph_finder.py +88 -73
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/styles_and_formulas.py +62 -9
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/typesetting.py +144 -37
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/translator/translator.py +0 -77
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/utils/layout_helper.py +3 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/high_level.py +9 -9
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/main.py +4 -26
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/CONTRIBUTING.md +11 -11
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/deploy.sh +1 -1
- {babeldoc-0.1.32 → babeldoc-0.2.0}/pyproject.toml +3 -2
- {babeldoc-0.1.32 → babeldoc-0.2.0}/uv.lock +17 -1
- babeldoc-0.1.32/babeldoc/__init__.py +0 -1
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.cursorignore +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/dependabot.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/labels.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/release-drafter.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/codeql.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/docs.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/labeler.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.gitignore +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/.pre-commit-config.yaml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/assets/assets.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/assets/embedding_assets_metadata.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/asynchronize/__init__.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/converter.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/backend/__init__.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/backend/pdf_creater.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/frontend/__init__.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/__init__.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/add_debug_information.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/layout_parser.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/remove_descent.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/translator/__init__.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/translator/cache.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/utils/fontmap.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/utils/style_helper.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/xml_converter.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/README.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/__init__.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/doclayout.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/rpc_doclayout.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/pdfinterp.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/progress_monitor.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/tools/generate_font_metadata.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/translation_config.py +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/CODE_OF_CONDUCT.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/CONTRIBUTOR_REWARD.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/README.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/README.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-banner.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-contributor_reward_example.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-preview.gif +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo.png +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/index.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/intro-to-pdf-object.md +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/requirements.txt +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/mkdocs.yml +0 -0
- {babeldoc-0.1.32 → babeldoc-0.2.0}/tests/test_translation_config.py +0 -0
|
@@ -37,9 +37,13 @@ jobs:
|
|
|
37
37
|
run: |
|
|
38
38
|
uv run babeldoc --warmup
|
|
39
39
|
- name: Run tests
|
|
40
|
+
env:
|
|
41
|
+
OPENAI_API_KEY: ${{ secrets.OPENAIAPIKEY }}
|
|
42
|
+
OPENAI_BASE_URL: ${{ secrets.OPENAIAPIURL }}
|
|
43
|
+
OPENAI_MODEL: ${{ secrets.OPENAIMODEL }}
|
|
40
44
|
run: |
|
|
41
45
|
uv run babeldoc --help
|
|
42
|
-
uv run babeldoc --
|
|
46
|
+
uv run babeldoc --openai --files examples/ci/test.pdf --openai-api-key ${{ env.OPENAI_API_KEY }} --openai-base-url ${{ env.OPENAI_BASE_URL }} --openai-model ${{ env.OPENAI_MODEL }}
|
|
43
47
|
- name: Generate offline assets package
|
|
44
48
|
run: |
|
|
45
49
|
uv run babeldoc --generate-offline-assets /tmp/offline_assets
|
|
@@ -629,7 +629,7 @@ to attach them to the start of each source file to most effectively
|
|
|
629
629
|
state the exclusion of warranty; and each file should have at least
|
|
630
630
|
the "copyright" line and a pointer to where the full notice is found.
|
|
631
631
|
|
|
632
|
-
|
|
632
|
+
BabelDOC is library for ultimated document translation solution.
|
|
633
633
|
Copyright (C) 2024 <funstory.ai limited>
|
|
634
634
|
|
|
635
635
|
This program is free software: you can redistribute it and/or modify
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BabelDOC
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: <3.13,>=3.10
|
|
13
13
|
Requires-Dist: bitstring>=4.3.0
|
|
14
14
|
Requires-Dist: configargparse>=1.7
|
|
15
|
+
Requires-Dist: freetype-py>=2.5.1
|
|
15
16
|
Requires-Dist: httpx[socks]>=0.27.0
|
|
16
17
|
Requires-Dist: huggingface-hub>=0.27.0
|
|
17
18
|
Requires-Dist: msgpack>=1.1.0
|
|
@@ -124,10 +125,10 @@ uv run babeldoc --help
|
|
|
124
125
|
3. Use the `uv run babeldoc` command. For example:
|
|
125
126
|
|
|
126
127
|
```bash
|
|
127
|
-
uv run babeldoc --
|
|
128
|
+
uv run babeldoc --files example.pdf --openai --openai-model "gpt-4o-mini" --openai-base-url "https://api.openai.com/v1" --openai-api-key "your-api-key-here"
|
|
128
129
|
|
|
129
130
|
# multiple files
|
|
130
|
-
uv run babeldoc --
|
|
131
|
+
uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model "gpt-4o-mini" --openai-base-url "https://api.openai.com/v1" --openai-api-key "your-api-key-here"
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
> [!TIP]
|
|
@@ -175,12 +176,10 @@ uv run babeldoc --bing --files example.pdf --files example2.pdf
|
|
|
175
176
|
- `--no-mono`: Do not output monolingual PDF files
|
|
176
177
|
- `--min-text-length`: Minimum text length to translate (default: 5)
|
|
177
178
|
- `--openai`: Use OpenAI for translation (default: False)
|
|
178
|
-
- `--bing`: Use Bing for translation (default: False)
|
|
179
|
-
- `--google`: Use Google Translate for translation (default: False)
|
|
180
179
|
|
|
181
180
|
> [!TIP]
|
|
182
181
|
>
|
|
183
|
-
> 1. You must specify one translation service among `--openai
|
|
182
|
+
> 1. You must specify one translation service among `--openai`.
|
|
184
183
|
> 2. It is recommended to use models with strong compatibility with OpenAI, such as: `glm-4-flash`, `deepseek-chat`, etc.
|
|
185
184
|
> 3. Currently, it has not been optimized for traditional translation engines like Bing/Google, it is recommended to use LLMs.
|
|
186
185
|
|
|
@@ -89,10 +89,10 @@ uv run babeldoc --help
|
|
|
89
89
|
3. Use the `uv run babeldoc` command. For example:
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
uv run babeldoc --
|
|
92
|
+
uv run babeldoc --files example.pdf --openai --openai-model "gpt-4o-mini" --openai-base-url "https://api.openai.com/v1" --openai-api-key "your-api-key-here"
|
|
93
93
|
|
|
94
94
|
# multiple files
|
|
95
|
-
uv run babeldoc --
|
|
95
|
+
uv run babeldoc --files example.pdf --files example2.pdf --openai --openai-model "gpt-4o-mini" --openai-base-url "https://api.openai.com/v1" --openai-api-key "your-api-key-here"
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
> [!TIP]
|
|
@@ -140,12 +140,10 @@ uv run babeldoc --bing --files example.pdf --files example2.pdf
|
|
|
140
140
|
- `--no-mono`: Do not output monolingual PDF files
|
|
141
141
|
- `--min-text-length`: Minimum text length to translate (default: 5)
|
|
142
142
|
- `--openai`: Use OpenAI for translation (default: False)
|
|
143
|
-
- `--bing`: Use Bing for translation (default: False)
|
|
144
|
-
- `--google`: Use Google Translate for translation (default: False)
|
|
145
143
|
|
|
146
144
|
> [!TIP]
|
|
147
145
|
>
|
|
148
|
-
> 1. You must specify one translation service among `--openai
|
|
146
|
+
> 1. You must specify one translation service among `--openai`.
|
|
149
147
|
> 2. It is recommended to use models with strong compatibility with OpenAI, such as: `glm-4-flash`, `deepseek-chat`, etc.
|
|
150
148
|
> 3. Currently, it has not been optimized for traditional translation engines like Bing/Google, it is recommended to use LLMs.
|
|
151
149
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -9,6 +9,7 @@ from babeldoc.document_il.il_version_1 import PageLayout
|
|
|
9
9
|
from babeldoc.document_il.il_version_1 import PdfCharacter
|
|
10
10
|
from babeldoc.document_il.il_version_1 import PdfFigure
|
|
11
11
|
from babeldoc.document_il.il_version_1 import PdfFont
|
|
12
|
+
from babeldoc.document_il.il_version_1 import PdfFontCharBoundingBox
|
|
12
13
|
from babeldoc.document_il.il_version_1 import PdfFormula
|
|
13
14
|
from babeldoc.document_il.il_version_1 import PdfLine
|
|
14
15
|
from babeldoc.document_il.il_version_1 import PdfParagraph
|
|
@@ -18,6 +19,7 @@ from babeldoc.document_il.il_version_1 import PdfSameStyleCharacters
|
|
|
18
19
|
from babeldoc.document_il.il_version_1 import PdfSameStyleUnicodeCharacters
|
|
19
20
|
from babeldoc.document_il.il_version_1 import PdfStyle
|
|
20
21
|
from babeldoc.document_il.il_version_1 import PdfXobject
|
|
22
|
+
from babeldoc.document_il.il_version_1 import VisualBbox
|
|
21
23
|
|
|
22
24
|
__all__ = [
|
|
23
25
|
"BaseOperations",
|
|
@@ -31,6 +33,7 @@ __all__ = [
|
|
|
31
33
|
"PdfCharacter",
|
|
32
34
|
"PdfFigure",
|
|
33
35
|
"PdfFont",
|
|
36
|
+
"PdfFontCharBoundingBox",
|
|
34
37
|
"PdfFormula",
|
|
35
38
|
"PdfLine",
|
|
36
39
|
"PdfParagraph",
|
|
@@ -40,4 +43,5 @@ __all__ = [
|
|
|
40
43
|
"PdfSameStyleUnicodeCharacters",
|
|
41
44
|
"PdfStyle",
|
|
42
45
|
"PdfXobject",
|
|
46
|
+
"VisualBbox",
|
|
43
47
|
]
|
|
@@ -2,7 +2,10 @@ import base64
|
|
|
2
2
|
import logging
|
|
3
3
|
import re
|
|
4
4
|
from functools import wraps
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from itertools import islice
|
|
5
7
|
|
|
8
|
+
import freetype
|
|
6
9
|
import pdfminer.pdfinterp
|
|
7
10
|
import pymupdf
|
|
8
11
|
from pdfminer.layout import LTChar
|
|
@@ -17,6 +20,18 @@ from pdfminer.psparser import PSLiteral
|
|
|
17
20
|
from babeldoc.document_il import il_version_1
|
|
18
21
|
from babeldoc.translation_config import TranslationConfig
|
|
19
22
|
|
|
23
|
+
|
|
24
|
+
def batched(iterable, n, *, strict=False):
|
|
25
|
+
# batched('ABCDEFG', 3) → ABC DEF G
|
|
26
|
+
if n < 1:
|
|
27
|
+
raise ValueError("n must be at least one")
|
|
28
|
+
iterator = iter(iterable)
|
|
29
|
+
while batch := tuple(islice(iterator, n)):
|
|
30
|
+
if strict and len(batch) != n:
|
|
31
|
+
raise ValueError("batched(): incomplete batch")
|
|
32
|
+
yield batch
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
logger = logging.getLogger(__name__)
|
|
21
36
|
|
|
22
37
|
|
|
@@ -40,6 +55,110 @@ PDFMinerPDFPage.__init__ = create_hook(
|
|
|
40
55
|
)
|
|
41
56
|
|
|
42
57
|
|
|
58
|
+
def indirect(obj):
|
|
59
|
+
if isinstance(obj, tuple) and obj[0] == "xref":
|
|
60
|
+
return int(obj[1].split(" ")[0])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_glyph_cbox(face, g):
|
|
64
|
+
face.load_glyph(g, freetype.FT_LOAD_NO_SCALE)
|
|
65
|
+
cbox = face.glyph.outline.get_bbox()
|
|
66
|
+
return cbox.xMin, cbox.yMin, cbox.xMax, cbox.yMax
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_char_cbox(face, idx):
|
|
70
|
+
g = face.get_char_index(idx)
|
|
71
|
+
return get_glyph_cbox(face, g)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_name_cbox(face, name):
|
|
75
|
+
g = face.get_name_index(name)
|
|
76
|
+
return get_glyph_cbox(face, g)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_font_file(doc, idx, encoding):
|
|
80
|
+
bbox_list = []
|
|
81
|
+
data = doc.xref_stream(idx)
|
|
82
|
+
face = freetype.Face(BytesIO(data))
|
|
83
|
+
for charmap in face.charmaps:
|
|
84
|
+
if charmap.encoding_name == "FT_ENCODING_ADOBE_CUSTOM":
|
|
85
|
+
face.select_charmap(freetype.FT_ENCODING_ADOBE_CUSTOM)
|
|
86
|
+
break
|
|
87
|
+
bbox_list = [get_char_cbox(face, x) for x in range(0, 256)]
|
|
88
|
+
if encoding:
|
|
89
|
+
for code, name in encoding:
|
|
90
|
+
bbox_list[code] = get_name_cbox(face, name.encode("U8"))
|
|
91
|
+
return bbox_list
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parse_encoding(obj_str):
|
|
95
|
+
delta = []
|
|
96
|
+
current = 0
|
|
97
|
+
for x in re.finditer(
|
|
98
|
+
r"(?P<p>[\[\]])|(?P<c>\d+)|(?P<n>/[a-zA-Z0-9]+)|(?P<s>.)", obj_str
|
|
99
|
+
):
|
|
100
|
+
key = x.lastgroup
|
|
101
|
+
val = x.group()
|
|
102
|
+
if key == "c":
|
|
103
|
+
current = int(val)
|
|
104
|
+
if key == "n":
|
|
105
|
+
delta.append((current, val[1:]))
|
|
106
|
+
current += 1
|
|
107
|
+
return delta
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_mapping(text):
|
|
111
|
+
mapping = []
|
|
112
|
+
for x in re.finditer(r"<(?P<num>[a-fA-F0-9]+)>", text):
|
|
113
|
+
mapping.append(int(x.group("num"), 16))
|
|
114
|
+
return mapping
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def update_cmap_pair(cmap, data):
|
|
118
|
+
for start, stop, value in batched(data, 3):
|
|
119
|
+
for code in range(start, stop + 1):
|
|
120
|
+
cmap[code] = value
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def update_cmap_code(cmap, data):
|
|
124
|
+
for code, value in batched(data, 2):
|
|
125
|
+
cmap[code] = value
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_cmap(cmap_str):
|
|
129
|
+
cmap = {}
|
|
130
|
+
for x in re.finditer(
|
|
131
|
+
r"\s+beginbfrange\s*(?P<r>(<[0-9a-fA-F]+>\s*)+)endbfrange\s+", cmap_str
|
|
132
|
+
):
|
|
133
|
+
update_cmap_pair(cmap, parse_mapping(x.group("r")))
|
|
134
|
+
for x in re.finditer(
|
|
135
|
+
r"\s+beginbfchar\s*(?P<c>(<[0-9a-fA-F]+>\s*)+)endbfchar", cmap_str
|
|
136
|
+
):
|
|
137
|
+
update_cmap_code(cmap, parse_mapping(x.group("c")))
|
|
138
|
+
return cmap
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_code(cmap, c):
|
|
142
|
+
for k, v in cmap.items():
|
|
143
|
+
if v == c:
|
|
144
|
+
return k
|
|
145
|
+
return -1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_bbox(bbox, size, c, x, y):
|
|
149
|
+
x_min, y_min, x_max, y_max = bbox[c]
|
|
150
|
+
factor = 1 / 1000 * size
|
|
151
|
+
x_min = x_min * factor
|
|
152
|
+
y_min = -y_min * factor
|
|
153
|
+
x_max = x_max * factor
|
|
154
|
+
y_max = -y_max * factor
|
|
155
|
+
ll = (x + x_min, y + y_min)
|
|
156
|
+
lr = (x + x_max, y + y_min)
|
|
157
|
+
ul = (x + x_min, y + y_max)
|
|
158
|
+
ur = (x + x_max, y + y_max)
|
|
159
|
+
return pymupdf.Quad(ll, lr, ul, ur)
|
|
160
|
+
|
|
161
|
+
|
|
43
162
|
class ILCreater:
|
|
44
163
|
stage_name = "Parse PDF and Create Intermediate Representation"
|
|
45
164
|
|
|
@@ -58,6 +177,8 @@ class ILCreater:
|
|
|
58
177
|
self.xobj_inc = 0
|
|
59
178
|
self.xobj_map: dict[int, il_version_1.PdfXobject] = {}
|
|
60
179
|
self.xobj_stack = []
|
|
180
|
+
self.current_page_font_name_id_map = {}
|
|
181
|
+
self.current_page_font_char_bounding_box_map = {}
|
|
61
182
|
|
|
62
183
|
def on_finish(self):
|
|
63
184
|
self.progress.__exit__(None, None, None)
|
|
@@ -121,12 +242,21 @@ class ILCreater:
|
|
|
121
242
|
|
|
122
243
|
def push_xobj(self):
|
|
123
244
|
self.xobj_stack.append(
|
|
124
|
-
(
|
|
245
|
+
(
|
|
246
|
+
self.current_page_font_name_id_map.copy(),
|
|
247
|
+
self.current_page_font_char_bounding_box_map.copy(),
|
|
248
|
+
self.xobj_id,
|
|
249
|
+
),
|
|
125
250
|
)
|
|
126
251
|
self.current_page_font_name_id_map = {}
|
|
252
|
+
self.current_page_font_char_bounding_box_map = {}
|
|
127
253
|
|
|
128
254
|
def pop_xobj(self):
|
|
129
|
-
|
|
255
|
+
(
|
|
256
|
+
self.current_page_font_name_id_map,
|
|
257
|
+
self.current_page_font_char_bounding_box_map,
|
|
258
|
+
self.xobj_id,
|
|
259
|
+
) = self.xobj_stack.pop()
|
|
130
260
|
|
|
131
261
|
def on_xobj_begin(self, bbox, xref_id):
|
|
132
262
|
self.push_passthrough_per_char_instruction()
|
|
@@ -164,6 +294,7 @@ class ILCreater:
|
|
|
164
294
|
unit="point",
|
|
165
295
|
)
|
|
166
296
|
self.current_page_font_name_id_map = {}
|
|
297
|
+
self.current_page_font_char_bounding_box_map = {}
|
|
167
298
|
self.passthrough_per_char_instruction_stack = []
|
|
168
299
|
self.xobj_stack = []
|
|
169
300
|
self.non_stroking_color_space_name = None
|
|
@@ -253,13 +384,73 @@ class ILCreater:
|
|
|
253
384
|
serif=serif,
|
|
254
385
|
ascent=font.ascent,
|
|
255
386
|
descent=font.descent,
|
|
387
|
+
pdf_font_char_bounding_box=[],
|
|
256
388
|
)
|
|
389
|
+
try:
|
|
390
|
+
bbox_list, cmap = self.parse_font_xobj_id(xref_id)
|
|
391
|
+
font_char_bounding_box_map = {}
|
|
392
|
+
if not cmap:
|
|
393
|
+
cmap = {x: x for x in range(257)}
|
|
394
|
+
for char_id in cmap:
|
|
395
|
+
if char_id < 0 or char_id >= len(bbox_list):
|
|
396
|
+
continue
|
|
397
|
+
bbox = bbox_list[char_id]
|
|
398
|
+
x, y, x2, y2 = bbox
|
|
399
|
+
if (
|
|
400
|
+
x == 0
|
|
401
|
+
and y == 0
|
|
402
|
+
and x2 == 500
|
|
403
|
+
and y2 == 698
|
|
404
|
+
or x == 0
|
|
405
|
+
and y == 0
|
|
406
|
+
and x2 == 0
|
|
407
|
+
and y2 == 0
|
|
408
|
+
):
|
|
409
|
+
# ignore default bounding box
|
|
410
|
+
continue
|
|
411
|
+
il_font_metadata.pdf_font_char_bounding_box.append(
|
|
412
|
+
il_version_1.PdfFontCharBoundingBox(
|
|
413
|
+
x=x,
|
|
414
|
+
y=y,
|
|
415
|
+
x2=x2,
|
|
416
|
+
y2=y2,
|
|
417
|
+
char_id=char_id,
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
font_char_bounding_box_map[char_id] = bbox
|
|
421
|
+
if self.xobj_id in self.xobj_map:
|
|
422
|
+
if self.xobj_id not in self.current_page_font_char_bounding_box_map:
|
|
423
|
+
self.current_page_font_char_bounding_box_map[self.xobj_id] = {}
|
|
424
|
+
self.current_page_font_char_bounding_box_map[self.xobj_id][font_id] = (
|
|
425
|
+
font_char_bounding_box_map
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
self.current_page_font_char_bounding_box_map[font_id] = (
|
|
429
|
+
font_char_bounding_box_map
|
|
430
|
+
)
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
257
433
|
self.current_page_font_name_id_map[font_name] = font_id
|
|
258
434
|
if self.xobj_id in self.xobj_map:
|
|
259
435
|
self.xobj_map[self.xobj_id].pdf_font.append(il_font_metadata)
|
|
260
436
|
else:
|
|
261
437
|
self.current_page.pdf_font.append(il_font_metadata)
|
|
262
438
|
|
|
439
|
+
def parse_font_xobj_id(self, xobj_id: int):
|
|
440
|
+
encoding = []
|
|
441
|
+
font_encoding = self.mupdf.xref_get_key(xobj_id, "Encoding/Differences")
|
|
442
|
+
if font_encoding:
|
|
443
|
+
encoding = parse_encoding(font_encoding[1])
|
|
444
|
+
bbox_list = []
|
|
445
|
+
font_file = self.mupdf.xref_get_key(xobj_id, "FontDescriptor/FontFile")
|
|
446
|
+
if file_idx := indirect(font_file):
|
|
447
|
+
bbox_list = parse_font_file(self.mupdf, file_idx, encoding)
|
|
448
|
+
cmap = {}
|
|
449
|
+
to_unicode = self.mupdf.xref_get_key(xobj_id, "ToUnicode")
|
|
450
|
+
if to_unicode_idx := indirect(to_unicode):
|
|
451
|
+
cmap = parse_cmap(self.mupdf.xref_stream(to_unicode_idx).decode("U8"))
|
|
452
|
+
return bbox_list, cmap
|
|
453
|
+
|
|
263
454
|
def create_graphic_state(self, gs: pdfminer.pdfinterp.PDFGraphicState):
|
|
264
455
|
graphic_state = il_version_1.GraphicState()
|
|
265
456
|
for k, v in gs.__dict__.items():
|
|
@@ -302,13 +493,27 @@ class ILCreater:
|
|
|
302
493
|
descent = font.descent * char.size / 1000
|
|
303
494
|
|
|
304
495
|
char_id = char.cid
|
|
496
|
+
|
|
497
|
+
if font_bounding_box_map := self.current_page_font_char_bounding_box_map.get(
|
|
498
|
+
self.xobj_id, self.current_page_font_char_bounding_box_map
|
|
499
|
+
).get(font.font_id):
|
|
500
|
+
char_bounding_box = font_bounding_box_map.get(char_id, None)
|
|
501
|
+
else:
|
|
502
|
+
char_bounding_box = None
|
|
503
|
+
|
|
305
504
|
char_unicode = char.get_text()
|
|
306
505
|
if "(cid:" not in char_unicode and len(char_unicode) > 1:
|
|
307
506
|
return
|
|
308
507
|
advance = char.adv
|
|
508
|
+
bbox = il_version_1.Box(
|
|
509
|
+
x=char.bbox[0],
|
|
510
|
+
y=char.bbox[1],
|
|
511
|
+
x2=char.bbox[2],
|
|
512
|
+
y2=char.bbox[3],
|
|
513
|
+
)
|
|
309
514
|
if char.matrix[0] == 0 and char.matrix[3] == 0:
|
|
310
515
|
vertical = True
|
|
311
|
-
|
|
516
|
+
visual_bbox = il_version_1.Box(
|
|
312
517
|
x=char.bbox[0] - descent,
|
|
313
518
|
y=char.bbox[1],
|
|
314
519
|
x2=char.bbox[2] - descent,
|
|
@@ -317,12 +522,13 @@ class ILCreater:
|
|
|
317
522
|
else:
|
|
318
523
|
vertical = False
|
|
319
524
|
# Add descent to y coordinates
|
|
320
|
-
|
|
525
|
+
visual_bbox = il_version_1.Box(
|
|
321
526
|
x=char.bbox[0],
|
|
322
527
|
y=char.bbox[1] + descent,
|
|
323
528
|
x2=char.bbox[2],
|
|
324
529
|
y2=char.bbox[3] + descent,
|
|
325
530
|
)
|
|
531
|
+
visual_bbox = il_version_1.VisualBbox(box=visual_bbox)
|
|
326
532
|
pdf_style = il_version_1.PdfStyle(
|
|
327
533
|
font_id=char.aw_font_id,
|
|
328
534
|
font_size=char.size,
|
|
@@ -336,6 +542,7 @@ class ILCreater:
|
|
|
336
542
|
vertical=vertical,
|
|
337
543
|
pdf_style=pdf_style,
|
|
338
544
|
xobj_id=char.xobj_id,
|
|
545
|
+
visual_bbox=visual_bbox,
|
|
339
546
|
)
|
|
340
547
|
if pdf_style.font_size == 0.0:
|
|
341
548
|
logger.warning(
|
|
@@ -343,6 +550,20 @@ class ILCreater:
|
|
|
343
550
|
char_unicode,
|
|
344
551
|
)
|
|
345
552
|
return
|
|
553
|
+
|
|
554
|
+
if char_bounding_box:
|
|
555
|
+
x_min, y_min, x_max, y_max = char_bounding_box
|
|
556
|
+
factor = 1 / 1000 * pdf_style.font_size
|
|
557
|
+
x_min = x_min * factor
|
|
558
|
+
y_min = y_min * factor
|
|
559
|
+
x_max = x_max * factor
|
|
560
|
+
y_max = y_max * factor
|
|
561
|
+
ll = (char.bbox[0] + x_min, char.bbox[1] + y_min)
|
|
562
|
+
ur = (char.bbox[0] + x_max, char.bbox[1] + y_max)
|
|
563
|
+
pdf_char.visual_bbox = il_version_1.VisualBbox(
|
|
564
|
+
il_version_1.Box(ll[0], ll[1], ur[0], ur[1])
|
|
565
|
+
)
|
|
566
|
+
|
|
346
567
|
self.current_page.pdf_character.append(pdf_char)
|
|
347
568
|
|
|
348
569
|
def create_il(self):
|