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.
Files changed (90) hide show
  1. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/test.yml +5 -1
  2. {babeldoc-0.1.32 → babeldoc-0.2.0}/LICENSE +1 -1
  3. {babeldoc-0.1.32 → babeldoc-0.2.0}/PKG-INFO +5 -6
  4. {babeldoc-0.1.32 → babeldoc-0.2.0}/README.md +3 -5
  5. babeldoc-0.2.0/babeldoc/__init__.py +1 -0
  6. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/const.py +1 -1
  7. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/__init__.py +4 -0
  8. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/frontend/il_creater.py +225 -4
  9. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.py +142 -73
  10. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.rnc +12 -2
  11. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.rng +27 -0
  12. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.xsd +20 -0
  13. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/il_translator.py +8 -8
  14. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/paragraph_finder.py +88 -73
  15. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/styles_and_formulas.py +62 -9
  16. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/typesetting.py +144 -37
  17. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/translator/translator.py +0 -77
  18. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/utils/layout_helper.py +3 -0
  19. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/high_level.py +9 -9
  20. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/main.py +4 -26
  21. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/CONTRIBUTING.md +11 -11
  22. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/deploy.sh +1 -1
  23. {babeldoc-0.1.32 → babeldoc-0.2.0}/pyproject.toml +3 -2
  24. {babeldoc-0.1.32 → babeldoc-0.2.0}/uv.lock +17 -1
  25. babeldoc-0.1.32/babeldoc/__init__.py +0 -1
  26. {babeldoc-0.1.32 → babeldoc-0.2.0}/.cursorignore +0 -0
  27. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  28. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  29. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/dependabot.yml +0 -0
  30. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/labels.yml +0 -0
  31. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/release-drafter.yml +0 -0
  32. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/codeql.yml +0 -0
  33. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/docs.yml +0 -0
  34. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/labeler.yml +0 -0
  35. {babeldoc-0.1.32 → babeldoc-0.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
  36. {babeldoc-0.1.32 → babeldoc-0.2.0}/.gitignore +0 -0
  37. {babeldoc-0.1.32 → babeldoc-0.2.0}/.pre-commit-config.yaml +0 -0
  38. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/assets/assets.py +0 -0
  39. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/assets/embedding_assets_metadata.py +0 -0
  40. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/asynchronize/__init__.py +0 -0
  41. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/converter.py +0 -0
  42. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
  43. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/backend/__init__.py +0 -0
  44. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/backend/pdf_creater.py +0 -0
  45. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/frontend/__init__.py +0 -0
  46. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/__init__.py +0 -0
  47. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/add_debug_information.py +0 -0
  48. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
  49. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/layout_parser.py +0 -0
  50. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/midend/remove_descent.py +0 -0
  51. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/translator/__init__.py +0 -0
  52. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/translator/cache.py +0 -0
  53. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/utils/fontmap.py +0 -0
  54. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/utils/style_helper.py +0 -0
  55. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/document_il/xml_converter.py +0 -0
  56. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/README.md +0 -0
  57. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/__init__.py +0 -0
  58. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/doclayout.py +0 -0
  59. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/docvision/rpc_doclayout.py +0 -0
  60. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/pdfinterp.py +0 -0
  61. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/progress_monitor.py +0 -0
  62. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/tools/generate_font_metadata.py +0 -0
  63. {babeldoc-0.1.32 → babeldoc-0.2.0}/babeldoc/translation_config.py +0 -0
  64. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/CODE_OF_CONDUCT.md +0 -0
  65. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/CONTRIBUTOR_REWARD.md +0 -0
  66. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
  67. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
  68. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
  69. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
  70. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
  71. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/README.md +0 -0
  72. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
  73. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
  74. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/README.md +0 -0
  75. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-banner.png +0 -0
  76. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
  77. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
  78. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
  79. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
  80. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo.png +0 -0
  81. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-contributor_reward_example.png +0 -0
  82. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-preview.gif +0 -0
  83. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
  84. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
  85. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo.png +0 -0
  86. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/index.md +0 -0
  87. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/intro-to-pdf-object.md +0 -0
  88. {babeldoc-0.1.32 → babeldoc-0.2.0}/docs/requirements.txt +0 -0
  89. {babeldoc-0.1.32 → babeldoc-0.2.0}/mkdocs.yml +0 -0
  90. {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 --bing --files examples/ci/test.pdf
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
- YADT is library for ultimated document translation solution.
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.1.32
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 --bing --files example.pdf
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 --bing --files example.pdf --files example2.pdf
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`, `--bing`, `--google`.
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 --bing --files example.pdf
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 --bing --files example.pdf --files example2.pdf
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`, `--bing`, `--google`.
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"
@@ -2,7 +2,7 @@ import shutil
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
 
5
- __version__ = "0.1.32"
5
+ __version__ = "0.2.0"
6
6
 
7
7
  CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"
8
8
 
@@ -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
- (self.current_page_font_name_id_map.copy(), self.xobj_id),
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
- self.current_page_font_name_id_map, self.xobj_id = self.xobj_stack.pop()
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
- bbox = il_version_1.Box(
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
- bbox = il_version_1.Box(
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):