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.
Files changed (42) hide show
  1. babeldoc/__init__.py +1 -0
  2. babeldoc/asynchronize/__init__.py +51 -0
  3. babeldoc/const.py +7 -0
  4. babeldoc/converter.py +510 -0
  5. babeldoc/document_il/__init__.py +43 -0
  6. babeldoc/document_il/backend/__init__.py +0 -0
  7. babeldoc/document_il/backend/pdf_creater.py +402 -0
  8. babeldoc/document_il/frontend/__init__.py +0 -0
  9. babeldoc/document_il/frontend/il_creater.py +348 -0
  10. babeldoc/document_il/il_version_1.py +799 -0
  11. babeldoc/document_il/il_version_1.rnc +141 -0
  12. babeldoc/document_il/il_version_1.rng +390 -0
  13. babeldoc/document_il/il_version_1.xsd +235 -0
  14. babeldoc/document_il/midend/__init__.py +0 -0
  15. babeldoc/document_il/midend/add_debug_information.py +138 -0
  16. babeldoc/document_il/midend/il_translator.py +553 -0
  17. babeldoc/document_il/midend/layout_parser.py +180 -0
  18. babeldoc/document_il/midend/paragraph_finder.py +452 -0
  19. babeldoc/document_il/midend/remove_descent.py +168 -0
  20. babeldoc/document_il/midend/styles_and_formulas.py +701 -0
  21. babeldoc/document_il/midend/typesetting.py +761 -0
  22. babeldoc/document_il/translator/__init__.py +0 -0
  23. babeldoc/document_il/translator/cache.py +145 -0
  24. babeldoc/document_il/translator/translator.py +267 -0
  25. babeldoc/document_il/utils/fontmap.py +227 -0
  26. babeldoc/document_il/utils/layout_helper.py +467 -0
  27. babeldoc/document_il/utils/style_helper.py +86 -0
  28. babeldoc/document_il/xml_converter.py +51 -0
  29. babeldoc/docvision/README.md +0 -0
  30. babeldoc/docvision/__init__.py +0 -0
  31. babeldoc/docvision/doclayout.py +278 -0
  32. babeldoc/docvision/rpc_doclayout.py +162 -0
  33. babeldoc/high_level.py +468 -0
  34. babeldoc/main.py +466 -0
  35. babeldoc/pdfinterp.py +464 -0
  36. babeldoc/progress_monitor.py +146 -0
  37. babeldoc/translation_config.py +156 -0
  38. babeldoc-0.1.6.dist-info/METADATA +283 -0
  39. babeldoc-0.1.6.dist-info/RECORD +42 -0
  40. babeldoc-0.1.6.dist-info/WHEEL +4 -0
  41. babeldoc-0.1.6.dist-info/entry_points.txt +2 -0
  42. babeldoc-0.1.6.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,402 @@
1
+ import logging
2
+ import re
3
+ from pathlib import Path
4
+
5
+ import pymupdf
6
+ from bitstring import BitStream
7
+
8
+ from babeldoc.document_il import il_version_1
9
+ from babeldoc.document_il.utils.fontmap import FontMapper
10
+ from babeldoc.translation_config import TranslateResult
11
+ from babeldoc.translation_config import TranslationConfig
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PDFCreater:
17
+ stage_name = "创建PDF文件"
18
+
19
+ def __init__(
20
+ self,
21
+ original_pdf_path: str,
22
+ document: il_version_1.Document,
23
+ translation_config: TranslationConfig,
24
+ ):
25
+ self.original_pdf_path = original_pdf_path
26
+ self.docs = document
27
+ self.font_path = translation_config.font
28
+ self.font_mapper = FontMapper(translation_config)
29
+ self.translation_config = translation_config
30
+
31
+ def render_graphic_state(
32
+ self,
33
+ draw_op: BitStream,
34
+ graphic_state: il_version_1.GraphicState,
35
+ ):
36
+ if graphic_state is None:
37
+ return
38
+ # if graphic_state.stroking_color_space_name:
39
+ # draw_op.append(
40
+ # f"/{graphic_state.stroking_color_space_name} CS \n".encode()
41
+ # )
42
+ # if graphic_state.non_stroking_color_space_name:
43
+ # draw_op.append(
44
+ # f"/{graphic_state.non_stroking_color_space_name}"
45
+ # f" cs \n".encode()
46
+ # )
47
+ # if graphic_state.ncolor is not None:
48
+ # if len(graphic_state.ncolor) == 1:
49
+ # draw_op.append(f"{graphic_state.ncolor[0]} g \n".encode())
50
+ # elif len(graphic_state.ncolor) == 3:
51
+ # draw_op.append(
52
+ # f"{' '.join((str(x) for x in graphic_state.ncolor))} sc \n".encode()
53
+ # )
54
+ # if graphic_state.scolor is not None:
55
+ # if len(graphic_state.scolor) == 1:
56
+ # draw_op.append(f"{graphic_state.scolor[0]} G \n".encode())
57
+ # elif len(graphic_state.scolor) == 3:
58
+ # draw_op.append(
59
+ # f"{' '.join((str(x) for x in graphic_state.scolor))} SC \n".encode()
60
+ # )
61
+
62
+ if graphic_state.passthrough_per_char_instruction:
63
+ draw_op.append(
64
+ f"{graphic_state.passthrough_per_char_instruction} \n".encode(),
65
+ )
66
+
67
+ def render_paragraph_to_char(
68
+ self,
69
+ paragraph: il_version_1.PdfParagraph,
70
+ ) -> list[il_version_1.PdfCharacter]:
71
+ chars = []
72
+ for composition in paragraph.pdf_paragraph_composition:
73
+ if not isinstance(composition.pdf_character, il_version_1.PdfCharacter):
74
+ logger.error(
75
+ f"Unknown composition type. "
76
+ f"This type only appears in the IL "
77
+ f"after the translation is completed."
78
+ f"During pdf rendering, this type is not supported."
79
+ f"Composition: {composition}. "
80
+ f"Paragraph: {paragraph}. ",
81
+ )
82
+ continue
83
+ chars.append(composition.pdf_character)
84
+ if not chars and paragraph.unicode:
85
+ logger.error(
86
+ f"Unable to export paragraphs that have "
87
+ f"not yet been formatted: {paragraph}",
88
+ )
89
+ return chars
90
+ return chars
91
+
92
+ def get_available_font_list(self, pdf, page):
93
+ page_xref_id = pdf[page.page_number].xref
94
+ return self.get_xobj_available_fonts(page_xref_id, pdf)
95
+
96
+ def get_xobj_available_fonts(self, page_xref_id, pdf):
97
+ resources_type, r_id = pdf.xref_get_key(page_xref_id, "Resources")
98
+ if resources_type == "xref":
99
+ resource_xref_id = re.search("(\\d+) 0 R", r_id).group(1)
100
+ r_id = pdf.xref_object(int(resource_xref_id))
101
+ resources_type = "dict"
102
+ if resources_type == "dict":
103
+ xref_id = re.search("/Font (\\d+) 0 R", r_id)
104
+ if xref_id is not None:
105
+ xref_id = xref_id.group(1)
106
+ font_dict = pdf.xref_object(int(xref_id))
107
+ else:
108
+ search = re.search("/Font *<<(.+?)>>", r_id.replace("\n", " "))
109
+ if search is None:
110
+ # Have resources but no fonts
111
+ return set()
112
+ font_dict = search.group(1)
113
+ else:
114
+ r_id = int(r_id.split(" ")[0])
115
+ _, font_dict = pdf.xref_get_key(r_id, "Font")
116
+ fonts = re.findall("/([^ ]+?) ", font_dict)
117
+ return set(fonts)
118
+
119
+ def _debug_render_rectangle(
120
+ self,
121
+ draw_op: BitStream,
122
+ rectangle: il_version_1.PdfRectangle,
123
+ ):
124
+ """Draw a debug rectangle in PDF for visualization purposes.
125
+
126
+ Args:
127
+ draw_op: BitStream to append PDF drawing operations
128
+ rectangle: Rectangle object containing position information
129
+ """
130
+ x1 = rectangle.box.x
131
+ y1 = rectangle.box.y
132
+ x2 = rectangle.box.x2
133
+ y2 = rectangle.box.y2
134
+ # Save graphics state
135
+ draw_op.append(b"q ")
136
+
137
+ # Set green color for debug visibility
138
+ draw_op.append(
139
+ rectangle.graphic_state.passthrough_per_char_instruction.encode(),
140
+ ) # Green stroke
141
+ draw_op.append(b" 1 w ") # Line width
142
+
143
+ # Draw four lines manually
144
+ # Bottom line
145
+ draw_op.append(f"{x1} {y1} m {x2} {y1} l S ".encode())
146
+ # Right line
147
+ draw_op.append(f"{x2} {y1} m {x2} {y2} l S ".encode())
148
+ # Top line
149
+ draw_op.append(f"{x2} {y2} m {x1} {y2} l S ".encode())
150
+ # Left line
151
+ draw_op.append(f"{x1} {y2} m {x1} {y1} l S ".encode())
152
+
153
+ # Restore graphics state
154
+ draw_op.append(b"Q\n")
155
+
156
+ def write_debug_info(
157
+ self,
158
+ pdf: pymupdf.Document,
159
+ translation_config: TranslationConfig,
160
+ ):
161
+ self.font_mapper.add_font(pdf, self.docs)
162
+
163
+ for page in self.docs.page:
164
+ _, r_id = pdf.xref_get_key(pdf[page.page_number].xref, "Contents")
165
+ resource_xref_id = re.search("(\\d+) 0 R", r_id).group(1)
166
+ base_op = pdf.xref_stream(int(resource_xref_id))
167
+ translation_config.raise_if_cancelled()
168
+ xobj_available_fonts = {}
169
+ xobj_draw_ops = {}
170
+ xobj_encoding_length_map = {}
171
+ available_font_list = self.get_available_font_list(pdf, page)
172
+
173
+ page_encoding_length_map = {
174
+ f.font_id: f.encoding_length for f in page.pdf_font
175
+ }
176
+ page_op = BitStream()
177
+ # q {ops_base}Q 1 0 0 1 {x0} {y0} cm {ops_new}
178
+ page_op.append(b"q ")
179
+ page_op.append(base_op)
180
+ page_op.append(b" Q ")
181
+ page_op.append(
182
+ f"q Q 1 0 0 1 {page.cropbox.box.x} {page.cropbox.box.y} cm \n".encode(),
183
+ )
184
+ # 收集所有字符
185
+ chars = []
186
+ # 首先添加页面级别的字符
187
+ if page.pdf_character:
188
+ chars.extend(page.pdf_character)
189
+ # 然后添加段落中的字符
190
+ for paragraph in page.pdf_paragraph:
191
+ chars.extend(self.render_paragraph_to_char(paragraph))
192
+
193
+ # 渲染所有字符
194
+ for char in chars:
195
+ if not getattr(char, "debug_info", False):
196
+ continue
197
+ if char.char_unicode == "\n":
198
+ continue
199
+ if char.pdf_character_id is None:
200
+ # dummy char
201
+ continue
202
+ char_size = char.pdf_style.font_size
203
+ font_id = char.pdf_style.font_id
204
+
205
+ if font_id not in available_font_list:
206
+ continue
207
+ draw_op = page_op
208
+ encoding_length_map = page_encoding_length_map
209
+
210
+ draw_op.append(b"q ")
211
+ self.render_graphic_state(draw_op, char.pdf_style.graphic_state)
212
+ if char.vertical:
213
+ draw_op.append(
214
+ f"BT /{font_id} {char_size:f} Tf 0 1 -1 0 {char.box.x2:f} {char.box.y:f} Tm ".encode(),
215
+ )
216
+ else:
217
+ draw_op.append(
218
+ f"BT /{font_id} {char_size:f} Tf 1 0 0 1 {char.box.x:f} {char.box.y:f} Tm ".encode(),
219
+ )
220
+
221
+ encoding_length = encoding_length_map[font_id]
222
+ # pdf32000-2008 page14:
223
+ # As hexadecimal data enclosed in angle brackets < >
224
+ # see 7.3.4.3, "Hexadecimal Strings."
225
+ draw_op.append(
226
+ f"<{char.pdf_character_id:0{encoding_length * 2}x}>".upper().encode(),
227
+ )
228
+
229
+ draw_op.append(b" Tj ET Q \n")
230
+ for rect in page.pdf_rectangle:
231
+ if not rect.debug_info:
232
+ continue
233
+ self._debug_render_rectangle(page_op, rect)
234
+ draw_op = page_op
235
+ # Since this is a draw instruction container,
236
+ # no additional information is needed
237
+ pdf.update_stream(int(resource_xref_id), draw_op.tobytes())
238
+ translation_config.raise_if_cancelled()
239
+ pdf.subset_fonts(fallback=False)
240
+
241
+ def write(self, translation_config: TranslationConfig) -> TranslateResult:
242
+ basename = Path(translation_config.input_file).stem
243
+ debug_suffix = ".debug" if translation_config.debug else ""
244
+ mono_out_path = translation_config.get_output_file_path(
245
+ f"{basename}{debug_suffix}.{translation_config.lang_out}.mono.pdf",
246
+ )
247
+ pdf = pymupdf.open(self.original_pdf_path)
248
+ self.font_mapper.add_font(pdf, self.docs)
249
+ with self.translation_config.progress_monitor.stage_start(
250
+ self.stage_name,
251
+ len(self.docs.page) + 2,
252
+ ) as pbar:
253
+ for page in self.docs.page:
254
+ translation_config.raise_if_cancelled()
255
+ xobj_available_fonts = {}
256
+ xobj_draw_ops = {}
257
+ xobj_encoding_length_map = {}
258
+ available_font_list = self.get_available_font_list(pdf, page)
259
+
260
+ for xobj in page.pdf_xobject:
261
+ xobj_available_fonts[xobj.xobj_id] = available_font_list.copy()
262
+ try:
263
+ xobj_available_fonts[xobj.xobj_id].update(
264
+ self.get_xobj_available_fonts(xobj.xref_id, pdf),
265
+ )
266
+ except Exception:
267
+ pass
268
+ xobj_encoding_length_map[xobj.xobj_id] = {
269
+ f.font_id: f.encoding_length for f in xobj.pdf_font
270
+ }
271
+ xobj_op = BitStream()
272
+ xobj_op.append(xobj.base_operations.value.encode())
273
+ xobj_draw_ops[xobj.xobj_id] = xobj_op
274
+ page_encoding_length_map = {
275
+ f.font_id: f.encoding_length for f in page.pdf_font
276
+ }
277
+ page_op = BitStream()
278
+ # q {ops_base}Q 1 0 0 1 {x0} {y0} cm {ops_new}
279
+ page_op.append(b"q ")
280
+ page_op.append(page.base_operations.value.encode())
281
+ page_op.append(b" Q ")
282
+ page_op.append(
283
+ f"q Q 1 0 0 1 {page.cropbox.box.x} {page.cropbox.box.y} cm \n".encode(),
284
+ )
285
+ # 收集所有字符
286
+ chars = []
287
+ # 首先添加页面级别的字符
288
+ if page.pdf_character:
289
+ chars.extend(page.pdf_character)
290
+ # 然后添加段落中的字符
291
+ for paragraph in page.pdf_paragraph:
292
+ chars.extend(self.render_paragraph_to_char(paragraph))
293
+
294
+ # 渲染所有字符
295
+ for char in chars:
296
+ if char.char_unicode == "\n":
297
+ continue
298
+ if char.pdf_character_id is None:
299
+ # dummy char
300
+ continue
301
+ char_size = char.pdf_style.font_size
302
+ font_id = char.pdf_style.font_id
303
+ if char.xobj_id in xobj_available_fonts:
304
+ if font_id not in xobj_available_fonts[char.xobj_id]:
305
+ continue
306
+ draw_op = xobj_draw_ops[char.xobj_id]
307
+ encoding_length_map = xobj_encoding_length_map[char.xobj_id]
308
+ else:
309
+ if font_id not in available_font_list:
310
+ continue
311
+ draw_op = page_op
312
+ encoding_length_map = page_encoding_length_map
313
+
314
+ draw_op.append(b"q ")
315
+ self.render_graphic_state(draw_op, char.pdf_style.graphic_state)
316
+ if char.vertical:
317
+ draw_op.append(
318
+ f"BT /{font_id} {char_size:f} Tf 0 1 -1 0 {char.box.x2:f} {char.box.y:f} Tm ".encode(),
319
+ )
320
+ else:
321
+ draw_op.append(
322
+ f"BT /{font_id} {char_size:f} Tf 1 0 0 1 {char.box.x:f} {char.box.y:f} Tm ".encode(),
323
+ )
324
+
325
+ encoding_length = encoding_length_map[font_id]
326
+ # pdf32000-2008 page14:
327
+ # As hexadecimal data enclosed in angle brackets < >
328
+ # see 7.3.4.3, "Hexadecimal Strings."
329
+ draw_op.append(
330
+ f"<{char.pdf_character_id:0{encoding_length * 2}x}>".upper().encode(),
331
+ )
332
+
333
+ draw_op.append(b" Tj ET Q \n")
334
+ for xobj in page.pdf_xobject:
335
+ draw_op = xobj_draw_ops[xobj.xobj_id]
336
+ pdf.update_stream(xobj.xref_id, draw_op.tobytes())
337
+ # pdf.update_stream(xobj.xref_id, b'')
338
+ for rect in page.pdf_rectangle:
339
+ self._debug_render_rectangle(page_op, rect)
340
+ draw_op = page_op
341
+ op_container = pdf.get_new_xref()
342
+ # Since this is a draw instruction container,
343
+ # no additional information is needed
344
+ pdf.update_object(op_container, "<<>>")
345
+ pdf.update_stream(op_container, draw_op.tobytes())
346
+ pdf[page.page_number].set_contents(op_container)
347
+ pbar.advance()
348
+ translation_config.raise_if_cancelled()
349
+ if not translation_config.skip_clean:
350
+ pdf.subset_fonts(fallback=False)
351
+ if not translation_config.no_mono:
352
+ if translation_config.debug:
353
+ translation_config.raise_if_cancelled()
354
+ pdf.save(
355
+ f"{mono_out_path}.decompressed.pdf",
356
+ expand=True,
357
+ pretty=True,
358
+ )
359
+ translation_config.raise_if_cancelled()
360
+ pdf.save(
361
+ mono_out_path,
362
+ garbage=3,
363
+ deflate=True,
364
+ clean=not translation_config.skip_clean,
365
+ deflate_fonts=True,
366
+ linear=True,
367
+ )
368
+ pbar.advance()
369
+ dual_out_path = None
370
+ if not translation_config.no_dual:
371
+ dual_out_path = translation_config.get_output_file_path(
372
+ f"{basename}{debug_suffix}.{translation_config.lang_out}.dual.pdf",
373
+ )
374
+ translation_config.raise_if_cancelled()
375
+ dual = pymupdf.open(self.original_pdf_path)
376
+ if translation_config.debug:
377
+ translation_config.raise_if_cancelled()
378
+ self.write_debug_info(dual, translation_config)
379
+ dual.insert_file(pdf)
380
+ page_count = pdf.page_count
381
+ for page_id in range(page_count):
382
+ if translation_config.dual_translate_first:
383
+ dual.move_page(page_count + page_id, page_id * 2)
384
+ else:
385
+ dual.move_page(page_count + page_id, page_id * 2 + 1)
386
+ dual.save(
387
+ dual_out_path,
388
+ garbage=3,
389
+ deflate=True,
390
+ clean=not translation_config.skip_clean,
391
+ deflate_fonts=True,
392
+ linear=True,
393
+ )
394
+ if translation_config.debug:
395
+ translation_config.raise_if_cancelled()
396
+ dual.save(
397
+ f"{dual_out_path}.decompressed.pdf",
398
+ expand=True,
399
+ pretty=True,
400
+ )
401
+ pbar.advance()
402
+ return TranslateResult(mono_out_path, dual_out_path)
File without changes