BabelDOC 0.1.31__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.31 → babeldoc-0.2.0}/.github/workflows/test.yml +5 -1
  2. {babeldoc-0.1.31 → babeldoc-0.2.0}/LICENSE +1 -1
  3. {babeldoc-0.1.31 → babeldoc-0.2.0}/PKG-INFO +5 -6
  4. {babeldoc-0.1.31 → babeldoc-0.2.0}/README.md +3 -5
  5. babeldoc-0.2.0/babeldoc/__init__.py +1 -0
  6. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/const.py +1 -1
  7. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/__init__.py +4 -0
  8. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/frontend/il_creater.py +249 -4
  9. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.py +142 -73
  10. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.rnc +12 -2
  11. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.rng +27 -0
  12. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/il_version_1.xsd +20 -0
  13. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/il_translator.py +8 -8
  14. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/paragraph_finder.py +88 -73
  15. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/styles_and_formulas.py +62 -9
  16. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/typesetting.py +144 -37
  17. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/translator/translator.py +0 -77
  18. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/utils/layout_helper.py +3 -0
  19. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/high_level.py +9 -9
  20. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/main.py +4 -26
  21. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/CONTRIBUTING.md +13 -11
  22. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/deploy.sh +1 -1
  23. {babeldoc-0.1.31 → babeldoc-0.2.0}/pyproject.toml +3 -2
  24. {babeldoc-0.1.31 → babeldoc-0.2.0}/uv.lock +17 -1
  25. babeldoc-0.1.31/babeldoc/__init__.py +0 -1
  26. {babeldoc-0.1.31 → babeldoc-0.2.0}/.cursorignore +0 -0
  27. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  28. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  29. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/dependabot.yml +0 -0
  30. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/labels.yml +0 -0
  31. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/release-drafter.yml +0 -0
  32. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/workflows/codeql.yml +0 -0
  33. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/workflows/docs.yml +0 -0
  34. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/workflows/labeler.yml +0 -0
  35. {babeldoc-0.1.31 → babeldoc-0.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
  36. {babeldoc-0.1.31 → babeldoc-0.2.0}/.gitignore +0 -0
  37. {babeldoc-0.1.31 → babeldoc-0.2.0}/.pre-commit-config.yaml +0 -0
  38. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/assets/assets.py +0 -0
  39. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/assets/embedding_assets_metadata.py +0 -0
  40. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/asynchronize/__init__.py +0 -0
  41. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/converter.py +0 -0
  42. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
  43. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/backend/__init__.py +0 -0
  44. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/backend/pdf_creater.py +0 -0
  45. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/frontend/__init__.py +0 -0
  46. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/__init__.py +0 -0
  47. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/add_debug_information.py +0 -0
  48. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
  49. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/layout_parser.py +0 -0
  50. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/midend/remove_descent.py +0 -0
  51. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/translator/__init__.py +0 -0
  52. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/translator/cache.py +0 -0
  53. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/utils/fontmap.py +0 -0
  54. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/utils/style_helper.py +0 -0
  55. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/document_il/xml_converter.py +0 -0
  56. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/docvision/README.md +0 -0
  57. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/docvision/__init__.py +0 -0
  58. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/docvision/doclayout.py +0 -0
  59. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/docvision/rpc_doclayout.py +0 -0
  60. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/pdfinterp.py +0 -0
  61. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/progress_monitor.py +0 -0
  62. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/tools/generate_font_metadata.py +0 -0
  63. {babeldoc-0.1.31 → babeldoc-0.2.0}/babeldoc/translation_config.py +0 -0
  64. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/CODE_OF_CONDUCT.md +0 -0
  65. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/CONTRIBUTOR_REWARD.md +0 -0
  66. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
  67. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
  68. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
  69. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
  70. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
  71. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/README.md +0 -0
  72. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
  73. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
  74. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/README.md +0 -0
  75. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-banner.png +0 -0
  76. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
  77. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
  78. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
  79. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
  80. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-big-logo.png +0 -0
  81. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-contributor_reward_example.png +0 -0
  82. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-preview.gif +0 -0
  83. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
  84. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
  85. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/images/babeldoc-small-logo.png +0 -0
  86. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/index.md +0 -0
  87. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/intro-to-pdf-object.md +0 -0
  88. {babeldoc-0.1.31 → babeldoc-0.2.0}/docs/requirements.txt +0 -0
  89. {babeldoc-0.1.31 → babeldoc-0.2.0}/mkdocs.yml +0 -0
  90. {babeldoc-0.1.31 → 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.31
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.31"
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
  ]
@@ -1,21 +1,164 @@
1
1
  import base64
2
2
  import logging
3
3
  import re
4
+ from functools import wraps
5
+ from io import BytesIO
6
+ from itertools import islice
4
7
 
8
+ import freetype
5
9
  import pdfminer.pdfinterp
6
10
  import pymupdf
7
11
  from pdfminer.layout import LTChar
8
12
  from pdfminer.layout import LTFigure
9
13
  from pdfminer.pdffont import PDFCIDFont
10
14
  from pdfminer.pdffont import PDFFont
15
+ from pdfminer.pdfpage import PDFPage as PDFMinerPDFPage
16
+ from pdfminer.pdftypes import PDFObjRef as PDFMinerPDFObjRef
17
+ from pdfminer.pdftypes import resolve1 as pdftypes_resolve1
11
18
  from pdfminer.psparser import PSLiteral
12
19
 
13
20
  from babeldoc.document_il import il_version_1
14
21
  from babeldoc.translation_config import TranslationConfig
15
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
+
16
35
  logger = logging.getLogger(__name__)
17
36
 
18
37
 
38
+ def create_hook(func, hook):
39
+ @wraps(func)
40
+ def wrapper(*args, **kwargs):
41
+ hook(*args, **kwargs)
42
+ return func(*args, **kwargs)
43
+
44
+ return wrapper
45
+
46
+
47
+ def hook_pdfminer_pdf_page_init(*args):
48
+ attrs = args[3]
49
+ while isinstance(attrs["MediaBox"], PDFMinerPDFObjRef):
50
+ attrs["MediaBox"] = pdftypes_resolve1(attrs["MediaBox"])
51
+
52
+
53
+ PDFMinerPDFPage.__init__ = create_hook(
54
+ PDFMinerPDFPage.__init__, hook_pdfminer_pdf_page_init
55
+ )
56
+
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
+
19
162
  class ILCreater:
20
163
  stage_name = "Parse PDF and Create Intermediate Representation"
21
164
 
@@ -34,6 +177,8 @@ class ILCreater:
34
177
  self.xobj_inc = 0
35
178
  self.xobj_map: dict[int, il_version_1.PdfXobject] = {}
36
179
  self.xobj_stack = []
180
+ self.current_page_font_name_id_map = {}
181
+ self.current_page_font_char_bounding_box_map = {}
37
182
 
38
183
  def on_finish(self):
39
184
  self.progress.__exit__(None, None, None)
@@ -97,12 +242,21 @@ class ILCreater:
97
242
 
98
243
  def push_xobj(self):
99
244
  self.xobj_stack.append(
100
- (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
+ ),
101
250
  )
102
251
  self.current_page_font_name_id_map = {}
252
+ self.current_page_font_char_bounding_box_map = {}
103
253
 
104
254
  def pop_xobj(self):
105
- 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()
106
260
 
107
261
  def on_xobj_begin(self, bbox, xref_id):
108
262
  self.push_passthrough_per_char_instruction()
@@ -140,6 +294,7 @@ class ILCreater:
140
294
  unit="point",
141
295
  )
142
296
  self.current_page_font_name_id_map = {}
297
+ self.current_page_font_char_bounding_box_map = {}
143
298
  self.passthrough_per_char_instruction_stack = []
144
299
  self.xobj_stack = []
145
300
  self.non_stroking_color_space_name = None
@@ -229,13 +384,73 @@ class ILCreater:
229
384
  serif=serif,
230
385
  ascent=font.ascent,
231
386
  descent=font.descent,
387
+ pdf_font_char_bounding_box=[],
232
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
233
433
  self.current_page_font_name_id_map[font_name] = font_id
234
434
  if self.xobj_id in self.xobj_map:
235
435
  self.xobj_map[self.xobj_id].pdf_font.append(il_font_metadata)
236
436
  else:
237
437
  self.current_page.pdf_font.append(il_font_metadata)
238
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
+
239
454
  def create_graphic_state(self, gs: pdfminer.pdfinterp.PDFGraphicState):
240
455
  graphic_state = il_version_1.GraphicState()
241
456
  for k, v in gs.__dict__.items():
@@ -278,13 +493,27 @@ class ILCreater:
278
493
  descent = font.descent * char.size / 1000
279
494
 
280
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
+
281
504
  char_unicode = char.get_text()
282
505
  if "(cid:" not in char_unicode and len(char_unicode) > 1:
283
506
  return
284
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
+ )
285
514
  if char.matrix[0] == 0 and char.matrix[3] == 0:
286
515
  vertical = True
287
- bbox = il_version_1.Box(
516
+ visual_bbox = il_version_1.Box(
288
517
  x=char.bbox[0] - descent,
289
518
  y=char.bbox[1],
290
519
  x2=char.bbox[2] - descent,
@@ -293,12 +522,13 @@ class ILCreater:
293
522
  else:
294
523
  vertical = False
295
524
  # Add descent to y coordinates
296
- bbox = il_version_1.Box(
525
+ visual_bbox = il_version_1.Box(
297
526
  x=char.bbox[0],
298
527
  y=char.bbox[1] + descent,
299
528
  x2=char.bbox[2],
300
529
  y2=char.bbox[3] + descent,
301
530
  )
531
+ visual_bbox = il_version_1.VisualBbox(box=visual_bbox)
302
532
  pdf_style = il_version_1.PdfStyle(
303
533
  font_id=char.aw_font_id,
304
534
  font_size=char.size,
@@ -312,6 +542,7 @@ class ILCreater:
312
542
  vertical=vertical,
313
543
  pdf_style=pdf_style,
314
544
  xobj_id=char.xobj_id,
545
+ visual_bbox=visual_bbox,
315
546
  )
316
547
  if pdf_style.font_size == 0.0:
317
548
  logger.warning(
@@ -319,6 +550,20 @@ class ILCreater:
319
550
  char_unicode,
320
551
  )
321
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
+
322
567
  self.current_page.pdf_character.append(pdf_char)
323
568
 
324
569
  def create_il(self):