BabelDOC 0.1.6__py3-none-any.whl
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/__init__.py +1 -0
- babeldoc/asynchronize/__init__.py +51 -0
- babeldoc/const.py +7 -0
- babeldoc/converter.py +510 -0
- babeldoc/document_il/__init__.py +43 -0
- babeldoc/document_il/backend/__init__.py +0 -0
- babeldoc/document_il/backend/pdf_creater.py +402 -0
- babeldoc/document_il/frontend/__init__.py +0 -0
- babeldoc/document_il/frontend/il_creater.py +348 -0
- babeldoc/document_il/il_version_1.py +799 -0
- babeldoc/document_il/il_version_1.rnc +141 -0
- babeldoc/document_il/il_version_1.rng +390 -0
- babeldoc/document_il/il_version_1.xsd +235 -0
- babeldoc/document_il/midend/__init__.py +0 -0
- babeldoc/document_il/midend/add_debug_information.py +138 -0
- babeldoc/document_il/midend/il_translator.py +553 -0
- babeldoc/document_il/midend/layout_parser.py +180 -0
- babeldoc/document_il/midend/paragraph_finder.py +452 -0
- babeldoc/document_il/midend/remove_descent.py +168 -0
- babeldoc/document_il/midend/styles_and_formulas.py +701 -0
- babeldoc/document_il/midend/typesetting.py +761 -0
- babeldoc/document_il/translator/__init__.py +0 -0
- babeldoc/document_il/translator/cache.py +145 -0
- babeldoc/document_il/translator/translator.py +267 -0
- babeldoc/document_il/utils/fontmap.py +227 -0
- babeldoc/document_il/utils/layout_helper.py +467 -0
- babeldoc/document_il/utils/style_helper.py +86 -0
- babeldoc/document_il/xml_converter.py +51 -0
- babeldoc/docvision/README.md +0 -0
- babeldoc/docvision/__init__.py +0 -0
- babeldoc/docvision/doclayout.py +278 -0
- babeldoc/docvision/rpc_doclayout.py +162 -0
- babeldoc/high_level.py +468 -0
- babeldoc/main.py +466 -0
- babeldoc/pdfinterp.py +464 -0
- babeldoc/progress_monitor.py +146 -0
- babeldoc/translation_config.py +156 -0
- babeldoc-0.1.6.dist-info/METADATA +283 -0
- babeldoc-0.1.6.dist-info/RECORD +42 -0
- babeldoc-0.1.6.dist-info/WHEEL +4 -0
- babeldoc-0.1.6.dist-info/entry_points.txt +2 -0
- babeldoc-0.1.6.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import pdfminer.pdfinterp
|
|
6
|
+
import pymupdf
|
|
7
|
+
from pdfminer.layout import LTChar
|
|
8
|
+
from pdfminer.layout import LTFigure
|
|
9
|
+
from pdfminer.pdffont import PDFCIDFont
|
|
10
|
+
from pdfminer.pdffont import PDFFont
|
|
11
|
+
from pdfminer.psparser import PSLiteral
|
|
12
|
+
|
|
13
|
+
from babeldoc.document_il import il_version_1
|
|
14
|
+
from babeldoc.translation_config import TranslationConfig
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ILCreater:
|
|
20
|
+
stage_name = "解析PDF并创建中间表示"
|
|
21
|
+
|
|
22
|
+
def __init__(self, translation_config: TranslationConfig):
|
|
23
|
+
self.progress = None
|
|
24
|
+
self.current_page: il_version_1.Page = None
|
|
25
|
+
self.mupdf: pymupdf.Document = None
|
|
26
|
+
self.model = translation_config.doc_layout_model
|
|
27
|
+
self.docs = il_version_1.Document(page=[])
|
|
28
|
+
self.stroking_color_space_name = None
|
|
29
|
+
self.non_stroking_color_space_name = None
|
|
30
|
+
self.passthrough_per_char_instruction: list[tuple[str, str]] = []
|
|
31
|
+
self.translation_config = translation_config
|
|
32
|
+
self.passthrough_per_char_instruction_stack: list[list[tuple[str, str]]] = []
|
|
33
|
+
self.xobj_id = 0
|
|
34
|
+
self.xobj_inc = 0
|
|
35
|
+
self.xobj_map: dict[int, il_version_1.PdfXobject] = {}
|
|
36
|
+
self.xobj_stack = []
|
|
37
|
+
|
|
38
|
+
def on_finish(self):
|
|
39
|
+
self.progress.__exit__(None, None, None)
|
|
40
|
+
|
|
41
|
+
def is_passthrough_per_char_operation(self, operator: str):
|
|
42
|
+
return re.match("^(sc|scn|g|rg|k|cs|gs|ri)$", operator, re.IGNORECASE)
|
|
43
|
+
|
|
44
|
+
def on_passthrough_per_char(self, operator: str, args: list[str]):
|
|
45
|
+
if not self.is_passthrough_per_char_operation(operator):
|
|
46
|
+
logger.error("Unknown passthrough_per_char operation: %s", operator)
|
|
47
|
+
return
|
|
48
|
+
# logger.debug("xobj_id: %d, on_passthrough_per_char: %s ( %s )", self.xobj_id, operator, args)
|
|
49
|
+
args = [self.parse_arg(arg) for arg in args]
|
|
50
|
+
for _i, value in enumerate(self.passthrough_per_char_instruction.copy()):
|
|
51
|
+
op, arg = value
|
|
52
|
+
if op == operator:
|
|
53
|
+
self.passthrough_per_char_instruction.remove(value)
|
|
54
|
+
break
|
|
55
|
+
self.passthrough_per_char_instruction.append((operator, " ".join(args)))
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def remove_latest_passthrough_per_char_instruction(self):
|
|
59
|
+
if self.passthrough_per_char_instruction:
|
|
60
|
+
self.passthrough_per_char_instruction.pop()
|
|
61
|
+
|
|
62
|
+
def parse_arg(self, arg: str):
|
|
63
|
+
if isinstance(arg, PSLiteral):
|
|
64
|
+
return f"/{arg.name}"
|
|
65
|
+
if not isinstance(arg, str):
|
|
66
|
+
return str(arg)
|
|
67
|
+
return arg
|
|
68
|
+
|
|
69
|
+
def pop_passthrough_per_char_instruction(self):
|
|
70
|
+
if self.passthrough_per_char_instruction_stack:
|
|
71
|
+
self.passthrough_per_char_instruction = (
|
|
72
|
+
self.passthrough_per_char_instruction_stack.pop()
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
self.passthrough_per_char_instruction = []
|
|
76
|
+
logging.error(
|
|
77
|
+
"pop_passthrough_per_char_instruction error on page: %s",
|
|
78
|
+
self.current_page.page_number,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def push_passthrough_per_char_instruction(self):
|
|
82
|
+
self.passthrough_per_char_instruction_stack.append(
|
|
83
|
+
self.passthrough_per_char_instruction.copy(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# pdf32000 page 171
|
|
87
|
+
def on_stroking_color_space(self, color_space_name):
|
|
88
|
+
self.stroking_color_space_name = color_space_name
|
|
89
|
+
|
|
90
|
+
def on_non_stroking_color_space(self, color_space_name):
|
|
91
|
+
self.non_stroking_color_space_name = color_space_name
|
|
92
|
+
|
|
93
|
+
def on_new_stream(self):
|
|
94
|
+
self.stroking_color_space_name = None
|
|
95
|
+
self.non_stroking_color_space_name = None
|
|
96
|
+
self.passthrough_per_char_instruction = []
|
|
97
|
+
|
|
98
|
+
def push_xobj(self):
|
|
99
|
+
self.xobj_stack.append(
|
|
100
|
+
(self.current_page_font_name_id_map.copy(), self.xobj_id),
|
|
101
|
+
)
|
|
102
|
+
self.current_page_font_name_id_map = {}
|
|
103
|
+
|
|
104
|
+
def pop_xobj(self):
|
|
105
|
+
self.current_page_font_name_id_map, self.xobj_id = self.xobj_stack.pop()
|
|
106
|
+
|
|
107
|
+
def on_xobj_begin(self, bbox, xref_id):
|
|
108
|
+
self.push_passthrough_per_char_instruction()
|
|
109
|
+
self.push_xobj()
|
|
110
|
+
self.xobj_inc += 1
|
|
111
|
+
self.xobj_id = self.xobj_inc
|
|
112
|
+
xobject = il_version_1.PdfXobject(
|
|
113
|
+
box=il_version_1.Box(
|
|
114
|
+
x=float(bbox[0]),
|
|
115
|
+
y=float(bbox[1]),
|
|
116
|
+
x2=float(bbox[2]),
|
|
117
|
+
y2=float(bbox[3]),
|
|
118
|
+
),
|
|
119
|
+
xobj_id=self.xobj_id,
|
|
120
|
+
xref_id=xref_id,
|
|
121
|
+
)
|
|
122
|
+
self.current_page.pdf_xobject.append(xobject)
|
|
123
|
+
self.xobj_map[self.xobj_id] = xobject
|
|
124
|
+
return self.xobj_id
|
|
125
|
+
|
|
126
|
+
def on_xobj_end(self, xobj_id, base_op):
|
|
127
|
+
self.pop_passthrough_per_char_instruction()
|
|
128
|
+
self.pop_xobj()
|
|
129
|
+
xobj = self.xobj_map[xobj_id]
|
|
130
|
+
xobj.base_operations = il_version_1.BaseOperations(value=base_op)
|
|
131
|
+
self.xobj_inc += 1
|
|
132
|
+
|
|
133
|
+
def on_page_start(self):
|
|
134
|
+
self.current_page = il_version_1.Page(
|
|
135
|
+
pdf_font=[],
|
|
136
|
+
pdf_character=[],
|
|
137
|
+
page_layout=[],
|
|
138
|
+
# currently don't support UserUnit page parameter
|
|
139
|
+
# pdf32000 page 79
|
|
140
|
+
unit="point",
|
|
141
|
+
)
|
|
142
|
+
self.current_page_font_name_id_map = {}
|
|
143
|
+
self.passthrough_per_char_instruction_stack = []
|
|
144
|
+
self.xobj_stack = []
|
|
145
|
+
self.non_stroking_color_space_name = None
|
|
146
|
+
self.stroking_color_space_name = None
|
|
147
|
+
self.docs.page.append(self.current_page)
|
|
148
|
+
|
|
149
|
+
def on_page_end(self):
|
|
150
|
+
self.progress.advance(1)
|
|
151
|
+
|
|
152
|
+
def on_page_crop_box(
|
|
153
|
+
self,
|
|
154
|
+
x0: float | int,
|
|
155
|
+
y0: float | int,
|
|
156
|
+
x1: float | int,
|
|
157
|
+
y1: float | int,
|
|
158
|
+
):
|
|
159
|
+
box = il_version_1.Box(x=float(x0), y=float(y0), x2=float(x1), y2=float(y1))
|
|
160
|
+
self.current_page.cropbox = il_version_1.Cropbox(box=box)
|
|
161
|
+
|
|
162
|
+
def on_page_media_box(
|
|
163
|
+
self,
|
|
164
|
+
x0: float | int,
|
|
165
|
+
y0: float | int,
|
|
166
|
+
x1: float | int,
|
|
167
|
+
y1: float | int,
|
|
168
|
+
):
|
|
169
|
+
box = il_version_1.Box(x=float(x0), y=float(y0), x2=float(x1), y2=float(y1))
|
|
170
|
+
self.current_page.mediabox = il_version_1.Mediabox(box=box)
|
|
171
|
+
|
|
172
|
+
def on_page_number(self, page_number: int):
|
|
173
|
+
assert isinstance(page_number, int)
|
|
174
|
+
assert page_number >= 0
|
|
175
|
+
self.current_page.page_number = page_number
|
|
176
|
+
|
|
177
|
+
def on_page_base_operation(self, operation: str):
|
|
178
|
+
self.current_page.base_operations = il_version_1.BaseOperations(value=operation)
|
|
179
|
+
|
|
180
|
+
def on_page_resource_font(self, font: PDFFont, xref_id: int, font_id: str):
|
|
181
|
+
font_name = font.fontname
|
|
182
|
+
if isinstance(font_name, bytes):
|
|
183
|
+
try:
|
|
184
|
+
font_name = font_name.decode("utf-8")
|
|
185
|
+
except UnicodeDecodeError:
|
|
186
|
+
font_name = "BASE64:" + base64.b64encode(font_name).decode("utf-8")
|
|
187
|
+
encoding_length = 1
|
|
188
|
+
if isinstance(font, PDFCIDFont):
|
|
189
|
+
try:
|
|
190
|
+
# pdf 32000:2008 page 273
|
|
191
|
+
# Table 118 - Predefined CJK CMap names
|
|
192
|
+
_, encoding = self.mupdf.xref_get_key(xref_id, "Encoding")
|
|
193
|
+
if encoding == "/Identity-H" or encoding == "/Identity-V":
|
|
194
|
+
encoding_length = 2
|
|
195
|
+
else:
|
|
196
|
+
_, to_unicode_id = self.mupdf.xref_get_key(xref_id, "ToUnicode")
|
|
197
|
+
to_unicode_bytes = self.mupdf.xref_stream(
|
|
198
|
+
int(to_unicode_id.split(" ")[0]),
|
|
199
|
+
)
|
|
200
|
+
code_range = re.search(
|
|
201
|
+
b"begincodespacerange\n?.*<(\\d+?)>.*",
|
|
202
|
+
to_unicode_bytes,
|
|
203
|
+
).group(1)
|
|
204
|
+
encoding_length = len(code_range) // 2
|
|
205
|
+
except Exception:
|
|
206
|
+
if max(font.unicode_map.cid2unichr.keys()) > 255:
|
|
207
|
+
encoding_length = 2
|
|
208
|
+
else:
|
|
209
|
+
encoding_length = 1
|
|
210
|
+
try:
|
|
211
|
+
mupdf_font = pymupdf.Font(fontbuffer=self.mupdf.extract_font(xref_id)[3])
|
|
212
|
+
bold = mupdf_font.is_bold
|
|
213
|
+
italic = mupdf_font.is_italic
|
|
214
|
+
monospaced = mupdf_font.is_monospaced
|
|
215
|
+
serif = mupdf_font.is_serif
|
|
216
|
+
except Exception:
|
|
217
|
+
bold = None
|
|
218
|
+
italic = None
|
|
219
|
+
monospaced = None
|
|
220
|
+
serif = None
|
|
221
|
+
il_font_metadata = il_version_1.PdfFont(
|
|
222
|
+
name=font_name,
|
|
223
|
+
xref_id=xref_id,
|
|
224
|
+
font_id=font_id,
|
|
225
|
+
encoding_length=encoding_length,
|
|
226
|
+
bold=bold,
|
|
227
|
+
italic=italic,
|
|
228
|
+
monospace=monospaced,
|
|
229
|
+
serif=serif,
|
|
230
|
+
ascent=font.ascent,
|
|
231
|
+
descent=font.descent,
|
|
232
|
+
)
|
|
233
|
+
self.current_page_font_name_id_map[font_name] = font_id
|
|
234
|
+
if self.xobj_id in self.xobj_map:
|
|
235
|
+
self.xobj_map[self.xobj_id].pdf_font.append(il_font_metadata)
|
|
236
|
+
else:
|
|
237
|
+
self.current_page.pdf_font.append(il_font_metadata)
|
|
238
|
+
|
|
239
|
+
def create_graphic_state(self, gs: pdfminer.pdfinterp.PDFGraphicState):
|
|
240
|
+
graphic_state = il_version_1.GraphicState()
|
|
241
|
+
for k, v in gs.__dict__.items():
|
|
242
|
+
if v is None:
|
|
243
|
+
continue
|
|
244
|
+
if k in ["scolor", "ncolor"]:
|
|
245
|
+
if isinstance(v, tuple):
|
|
246
|
+
v = list(v)
|
|
247
|
+
else:
|
|
248
|
+
v = [v]
|
|
249
|
+
setattr(graphic_state, k, v)
|
|
250
|
+
continue
|
|
251
|
+
if k == "linewidth":
|
|
252
|
+
graphic_state.linewidth = float(v)
|
|
253
|
+
continue
|
|
254
|
+
continue
|
|
255
|
+
raise NotImplementedError
|
|
256
|
+
|
|
257
|
+
graphic_state.stroking_color_space_name = self.stroking_color_space_name
|
|
258
|
+
graphic_state.non_stroking_color_space_name = self.non_stroking_color_space_name
|
|
259
|
+
|
|
260
|
+
graphic_state.passthrough_per_char_instruction = " ".join(
|
|
261
|
+
f"{arg} {op}" for op, arg in gs.passthrough_instruction
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return graphic_state
|
|
265
|
+
|
|
266
|
+
def on_lt_char(self, char: LTChar):
|
|
267
|
+
gs = self.create_graphic_state(char.graphicstate)
|
|
268
|
+
# Get font from current page or xobject
|
|
269
|
+
font = None
|
|
270
|
+
for pdf_font in self.xobj_map.get(self.xobj_id, self.current_page).pdf_font:
|
|
271
|
+
if pdf_font.font_id == char.aw_font_id:
|
|
272
|
+
font = pdf_font
|
|
273
|
+
break
|
|
274
|
+
|
|
275
|
+
# Get descent from font
|
|
276
|
+
descent = 0
|
|
277
|
+
if font and hasattr(font, "descent"):
|
|
278
|
+
descent = font.descent * char.size / 1000
|
|
279
|
+
|
|
280
|
+
char_id = char.cid
|
|
281
|
+
char_unicode = char.get_text()
|
|
282
|
+
if "(cid:" not in char_unicode and len(char_unicode) > 1:
|
|
283
|
+
return
|
|
284
|
+
advance = char.adv
|
|
285
|
+
if char.matrix[0] == 0 and char.matrix[3] == 0:
|
|
286
|
+
vertical = True
|
|
287
|
+
bbox = il_version_1.Box(
|
|
288
|
+
x=char.bbox[0] - descent,
|
|
289
|
+
y=char.bbox[1],
|
|
290
|
+
x2=char.bbox[2] - descent,
|
|
291
|
+
y2=char.bbox[3],
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
vertical = False
|
|
295
|
+
# Add descent to y coordinates
|
|
296
|
+
bbox = il_version_1.Box(
|
|
297
|
+
x=char.bbox[0],
|
|
298
|
+
y=char.bbox[1] + descent,
|
|
299
|
+
x2=char.bbox[2],
|
|
300
|
+
y2=char.bbox[3] + descent,
|
|
301
|
+
)
|
|
302
|
+
pdf_style = il_version_1.PdfStyle(
|
|
303
|
+
font_id=char.aw_font_id,
|
|
304
|
+
font_size=char.size,
|
|
305
|
+
graphic_state=gs,
|
|
306
|
+
)
|
|
307
|
+
pdf_char = il_version_1.PdfCharacter(
|
|
308
|
+
box=bbox,
|
|
309
|
+
pdf_character_id=char_id,
|
|
310
|
+
advance=advance,
|
|
311
|
+
char_unicode=char_unicode,
|
|
312
|
+
vertical=vertical,
|
|
313
|
+
pdf_style=pdf_style,
|
|
314
|
+
xobj_id=char.xobj_id,
|
|
315
|
+
)
|
|
316
|
+
self.current_page.pdf_character.append(pdf_char)
|
|
317
|
+
|
|
318
|
+
def create_il(self):
|
|
319
|
+
pages = [
|
|
320
|
+
page
|
|
321
|
+
for page in self.docs.page
|
|
322
|
+
if self.translation_config.should_translate_page(page.page_number + 1)
|
|
323
|
+
]
|
|
324
|
+
self.docs.page = pages
|
|
325
|
+
return self.docs
|
|
326
|
+
|
|
327
|
+
def on_total_pages(self, total_pages: int):
|
|
328
|
+
assert isinstance(total_pages, int)
|
|
329
|
+
assert total_pages > 0
|
|
330
|
+
self.docs.total_pages = total_pages
|
|
331
|
+
total = 0
|
|
332
|
+
for page in range(total_pages):
|
|
333
|
+
if self.translation_config.should_translate_page(page + 1) is False:
|
|
334
|
+
continue
|
|
335
|
+
total += 1
|
|
336
|
+
self.progress = self.translation_config.progress_monitor.stage_start(
|
|
337
|
+
self.stage_name,
|
|
338
|
+
total,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def on_pdf_figure(self, figure: LTFigure):
|
|
342
|
+
box = il_version_1.Box(
|
|
343
|
+
figure.bbox[0],
|
|
344
|
+
figure.bbox[1],
|
|
345
|
+
figure.bbox[2],
|
|
346
|
+
figure.bbox[3],
|
|
347
|
+
)
|
|
348
|
+
self.current_page.pdf_figure.append(il_version_1.PdfFigure(box=box))
|