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,761 @@
1
+ import logging
2
+ import statistics
3
+ import unicodedata
4
+ from functools import cache
5
+
6
+ import pymupdf
7
+
8
+ from babeldoc.document_il import Box
9
+ from babeldoc.document_il import PdfCharacter
10
+ from babeldoc.document_il import PdfFormula
11
+ from babeldoc.document_il import PdfParagraphComposition
12
+ from babeldoc.document_il import PdfStyle
13
+ from babeldoc.document_il import il_version_1
14
+ from babeldoc.document_il.utils.fontmap import FontMapper
15
+ from babeldoc.translation_config import TranslationConfig
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class TypesettingUnit:
21
+ def __str__(self):
22
+ return self.try_get_unicode()
23
+
24
+ def __init__(
25
+ self,
26
+ char: PdfCharacter = None,
27
+ formular: PdfFormula = None,
28
+ unicode: str = None,
29
+ font: pymupdf.Font | None = None,
30
+ font_size: float = None,
31
+ style: PdfStyle = None,
32
+ xobj_id: int = None,
33
+ debug_info: bool = False,
34
+ ):
35
+ assert sum(x is not None for x in [char, formular, unicode]) == 1, (
36
+ "Only one of chars and formular can be not None"
37
+ )
38
+ self.char = char
39
+ self.formular = formular
40
+ self.unicode = unicode
41
+ self.x = None
42
+ self.y = None
43
+ self.scale = None
44
+ self.debug_info = debug_info
45
+
46
+ if unicode:
47
+ assert font_size, "Font size must be provided when unicode is provided"
48
+ assert style, "Style must be provided when unicode is provided"
49
+ assert len(unicode) == 1, "Unicode must be a single character"
50
+ assert xobj_id is not None, (
51
+ "Xobj id must be provided when unicode is provided"
52
+ )
53
+
54
+ self.font = font
55
+ self.font_id = font.font_id
56
+ self.font_size = font_size
57
+ self.style = style
58
+ self.xobj_id = xobj_id
59
+
60
+ def try_get_unicode(self) -> str | None:
61
+ if self.char:
62
+ return self.char.char_unicode
63
+ elif self.formular:
64
+ return None
65
+ elif self.unicode:
66
+ return self.unicode
67
+
68
+ @property
69
+ def mixed_character_blacklist(self):
70
+ unicode = self.try_get_unicode()
71
+ if unicode:
72
+ return unicode in [
73
+ "。",
74
+ ",",
75
+ ":",
76
+ "?",
77
+ "!",
78
+ ]
79
+ return False
80
+
81
+ @property
82
+ def is_chinese_char(self):
83
+ if self.formular:
84
+ return False
85
+ unicode = self.try_get_unicode()
86
+ if not unicode:
87
+ return False
88
+ if "(cid" in unicode:
89
+ return False
90
+ if len(unicode) > 1:
91
+ return False
92
+ assert len(unicode) == 1, "Unicode must be a single character"
93
+ if unicode in [
94
+ "(",
95
+ ")",
96
+ "【",
97
+ "】",
98
+ "《",
99
+ "》",
100
+ "〔",
101
+ "〕",
102
+ "〈",
103
+ "〉",
104
+ "〖",
105
+ "〗",
106
+ "「",
107
+ "」",
108
+ "『",
109
+ "』",
110
+ "、",
111
+ "。",
112
+ ":",
113
+ "?",
114
+ "!",
115
+ ",",
116
+ ]:
117
+ return True
118
+ if unicode:
119
+ try:
120
+ unicodedata_name = unicodedata.name(unicode)
121
+ return (
122
+ "CJK UNIFIED IDEOGRAPH" in unicodedata_name
123
+ or "FULLWIDTH" in unicodedata_name
124
+ )
125
+ except ValueError:
126
+ return False
127
+ return False
128
+
129
+ @property
130
+ def is_space(self):
131
+ if self.formular:
132
+ return False
133
+ unicode = self.try_get_unicode()
134
+ return unicode == " "
135
+
136
+ @property
137
+ def is_hung_punctuation(self):
138
+ if self.formular:
139
+ return False
140
+ unicode = self.try_get_unicode()
141
+
142
+ if unicode:
143
+ return unicode in [
144
+ ",",
145
+ ".",
146
+ ":",
147
+ ";",
148
+ "?",
149
+ "!",
150
+ ",",
151
+ "。",
152
+ ":",
153
+ "?",
154
+ "!",
155
+ "]",
156
+ "}",
157
+ ")",
158
+ "〕",
159
+ "〉",
160
+ "】",
161
+ "〗",
162
+ "」",
163
+ "』",
164
+ "、",
165
+ "”",
166
+ '"',
167
+ ";",
168
+ ]
169
+ return False
170
+
171
+ def passthrough(self) -> [PdfCharacter]:
172
+ if self.char:
173
+ return [self.char]
174
+ elif self.formular:
175
+ return self.formular.pdf_character
176
+ elif self.unicode:
177
+ logger.error(f"Cannot passthrough unicode. TypesettingUnit: {self}. ")
178
+ logger.error(f"Cannot passthrough unicode. TypesettingUnit: {self}. ")
179
+ return []
180
+
181
+ @property
182
+ def can_passthrough(self):
183
+ return self.unicode is None
184
+
185
+ @property
186
+ def box(self):
187
+ if self.char:
188
+ return self.char.box
189
+ elif self.formular:
190
+ return self.formular.box
191
+ elif self.unicode:
192
+ char_width = self.font.char_lengths(self.unicode, self.font_size)[0]
193
+ if self.x is None or self.y is None or self.scale is None:
194
+ return Box(0, 0, char_width, self.font_size)
195
+ return Box(self.x, self.y, self.x + char_width, self.y + self.font_size)
196
+
197
+ @property
198
+ def width(self):
199
+ return self.box.x2 - self.box.x
200
+
201
+ @property
202
+ def height(self):
203
+ return self.box.y2 - self.box.y
204
+
205
+ def relocate(self, x: float, y: float, scale: float) -> "TypesettingUnit":
206
+ """重定位并缩放排版单元
207
+
208
+ Args:
209
+ x: 新的 x 坐标
210
+ y: 新的 y 坐标
211
+ scale: 缩放因子
212
+
213
+ Returns:
214
+ 新的排版单元
215
+ """
216
+ if self.char:
217
+ # 创建新的字符对象
218
+ new_char = PdfCharacter(
219
+ pdf_character_id=self.char.pdf_character_id,
220
+ char_unicode=self.char.char_unicode,
221
+ box=Box(
222
+ x=x,
223
+ y=y,
224
+ x2=x + self.width * scale,
225
+ y2=y + self.height * scale,
226
+ ),
227
+ pdf_style=PdfStyle(
228
+ font_id=self.char.pdf_style.font_id,
229
+ font_size=self.char.pdf_style.font_size * scale,
230
+ graphic_state=self.char.pdf_style.graphic_state,
231
+ ),
232
+ scale=scale,
233
+ vertical=self.char.vertical,
234
+ advance=self.char.advance * scale if self.char.advance else None,
235
+ debug_info=self.debug_info,
236
+ )
237
+ return TypesettingUnit(char=new_char)
238
+
239
+ elif self.formular:
240
+ # 创建新的公式对象,保持内部字符的相对位置
241
+ new_chars = []
242
+ min_x = min(char.box.x for char in self.formular.pdf_character)
243
+ min_y = min(char.box.y for char in self.formular.pdf_character)
244
+
245
+ for char in self.formular.pdf_character:
246
+ # 计算相对位置
247
+ rel_x = char.box.x - min_x
248
+ rel_y = char.box.y - min_y
249
+
250
+ # 创建新的字符对象
251
+ new_char = PdfCharacter(
252
+ pdf_character_id=char.pdf_character_id,
253
+ char_unicode=char.char_unicode,
254
+ box=Box(
255
+ x=x + (rel_x + self.formular.x_offset) * scale,
256
+ y=y + (rel_y + self.formular.y_offset) * scale,
257
+ x2=x
258
+ + (rel_x + (char.box.x2 - char.box.x) + self.formular.x_offset)
259
+ * scale,
260
+ y2=y
261
+ + (rel_y + (char.box.y2 - char.box.y) + self.formular.y_offset)
262
+ * scale,
263
+ ),
264
+ pdf_style=PdfStyle(
265
+ font_id=char.pdf_style.font_id,
266
+ font_size=char.pdf_style.font_size * scale,
267
+ graphic_state=char.pdf_style.graphic_state,
268
+ ),
269
+ scale=scale,
270
+ vertical=char.vertical,
271
+ advance=char.advance * scale if char.advance else None,
272
+ )
273
+ new_chars.append(new_char)
274
+
275
+ # Calculate bounding box from new_chars
276
+ min_x = min(char.box.x for char in new_chars)
277
+ min_y = min(char.box.y for char in new_chars)
278
+ max_x = max(char.box.x2 for char in new_chars)
279
+ max_y = max(char.box.y2 for char in new_chars)
280
+
281
+ new_formula = PdfFormula(
282
+ box=Box(
283
+ x=min_x,
284
+ y=min_y,
285
+ x2=max_x,
286
+ y2=max_y,
287
+ ),
288
+ pdf_character=new_chars,
289
+ x_offset=self.formular.x_offset * scale,
290
+ y_offset=self.formular.y_offset * scale,
291
+ )
292
+ return TypesettingUnit(formular=new_formula)
293
+
294
+ elif self.unicode:
295
+ # 对于 Unicode 字符,我们存储新的位置信息
296
+ new_unit = TypesettingUnit(
297
+ unicode=self.unicode,
298
+ font=self.font,
299
+ font_size=self.font_size * scale,
300
+ style=self.style,
301
+ xobj_id=self.xobj_id,
302
+ debug_info=self.debug_info,
303
+ )
304
+ new_unit.x = x
305
+ new_unit.y = y
306
+ new_unit.scale = scale
307
+ return new_unit
308
+
309
+ def render(self) -> [PdfCharacter]:
310
+ """渲染排版单元为 PdfCharacter 列表
311
+
312
+ Returns:
313
+ PdfCharacter 列表
314
+ """
315
+ if self.can_passthrough:
316
+ return self.passthrough()
317
+ elif self.unicode:
318
+ assert self.x is not None, (
319
+ "x position must be set, should be set by `relocate`"
320
+ )
321
+ assert self.y is not None, (
322
+ "y position must be set, should be set by `relocate`"
323
+ )
324
+ assert self.scale is not None, (
325
+ "scale must be set, should be set by `relocate`"
326
+ )
327
+ # 计算字符宽度
328
+ char_width = self.width
329
+
330
+ new_char = PdfCharacter(
331
+ pdf_character_id=self.font.has_glyph(ord(self.unicode)),
332
+ char_unicode=self.unicode,
333
+ box=Box(
334
+ x=self.x, # 使用存储的位置
335
+ y=self.y,
336
+ x2=self.x + char_width,
337
+ y2=self.y + self.font_size,
338
+ ),
339
+ pdf_style=PdfStyle(
340
+ font_id=self.font_id,
341
+ font_size=self.font_size,
342
+ graphic_state=self.style.graphic_state,
343
+ ),
344
+ scale=self.scale,
345
+ vertical=False,
346
+ advance=char_width,
347
+ xobj_id=self.xobj_id,
348
+ debug_info=self.debug_info,
349
+ )
350
+ return [new_char]
351
+ else:
352
+ logger.error(f"Unknown typesetting unit. TypesettingUnit: {self}. ")
353
+ logger.error(f"Unknown typesetting unit. TypesettingUnit: {self}. ")
354
+ return []
355
+
356
+
357
+ class Typesetting:
358
+ stage_name = "排版"
359
+
360
+ def __init__(self, translation_config: TranslationConfig):
361
+ self.font_mapper = FontMapper(translation_config)
362
+ self.translation_config = translation_config
363
+
364
+ def typsetting_document(self, document: il_version_1.Document):
365
+ with self.translation_config.progress_monitor.stage_start(
366
+ self.stage_name,
367
+ len(document.page),
368
+ ) as pbar:
369
+ for page in document.page:
370
+ self.translation_config.raise_if_cancelled()
371
+ self.render_page(page)
372
+ pbar.advance()
373
+
374
+ def render_page(self, page: il_version_1.Page):
375
+ fonts: dict[
376
+ str | int,
377
+ il_version_1.PdfFont | dict[str, il_version_1.PdfFont],
378
+ ] = {f.font_id: f for f in page.pdf_font}
379
+ page_fonts = {f.font_id: f for f in page.pdf_font}
380
+ for k, v in self.font_mapper.fontid2font.items():
381
+ fonts[k] = v
382
+ for xobj in page.pdf_xobject:
383
+ fonts[xobj.xobj_id] = page_fonts.copy()
384
+ for font in xobj.pdf_font:
385
+ fonts[xobj.xobj_id][font.font_id] = font
386
+ if page.page_number == 0:
387
+ self.add_watermark(page)
388
+ # 开始实际的渲染过程
389
+ for paragraph in page.pdf_paragraph:
390
+ self.render_paragraph(paragraph, page, fonts)
391
+
392
+ def add_watermark(self, page: il_version_1.Page):
393
+ page_width = page.cropbox.box.x2 - page.cropbox.box.x
394
+ page_height = page.cropbox.box.y2 - page.cropbox.box.y
395
+ style = il_version_1.PdfStyle(
396
+ font_id="base",
397
+ font_size=6,
398
+ graphic_state=il_version_1.GraphicState(),
399
+ )
400
+ text = "本文档由funstory.ai的开源PDF翻译库yadt(http://yadt.io)翻译,本仓库正在积极的建设当中,欢迎star和关注。"
401
+ if self.translation_config.debug:
402
+ text += "\n 当前为DEBUG模式,将显示更多辅助信息。请注意,部分框的位置对应原文,但在译文中可能不正确。"
403
+ page.pdf_paragraph.append(
404
+ il_version_1.PdfParagraph(
405
+ first_line_indent=False,
406
+ box=il_version_1.Box(
407
+ x=page.cropbox.box.x + page_width * 0.05,
408
+ y=page.cropbox.box.y,
409
+ x2=page.cropbox.box.x2,
410
+ y2=page.cropbox.box.y2 - page_height * 0.05,
411
+ ),
412
+ vertical=False,
413
+ pdf_style=style,
414
+ pdf_paragraph_composition=[
415
+ il_version_1.PdfParagraphComposition(
416
+ pdf_same_style_unicode_characters=il_version_1.PdfSameStyleUnicodeCharacters(
417
+ unicode=text,
418
+ pdf_style=style,
419
+ ),
420
+ ),
421
+ ],
422
+ xobj_id=-1,
423
+ ),
424
+ )
425
+
426
+ def render_paragraph(
427
+ self,
428
+ paragraph: il_version_1.PdfParagraph,
429
+ page: il_version_1.Page,
430
+ fonts: dict[
431
+ str | int,
432
+ il_version_1.PdfFont | dict[str, il_version_1.PdfFont],
433
+ ],
434
+ ):
435
+ typesetting_units = self.create_typesetting_units(paragraph, fonts)
436
+ # 如果所有单元都可以直接传递,则直接传递
437
+ if all(unit.can_passthrough for unit in typesetting_units):
438
+ paragraph.scale = 1.0
439
+ paragraph.pdf_paragraph_composition = self.create_passthrough_composition(
440
+ typesetting_units,
441
+ )
442
+ return
443
+
444
+ # 如果有单元无法直接传递,则进行重排版
445
+ paragraph.pdf_paragraph_composition = []
446
+ self.retypeset(paragraph, page, typesetting_units)
447
+
448
+ def _layout_typesetting_units(
449
+ self,
450
+ typesetting_units: list[TypesettingUnit],
451
+ box: Box,
452
+ scale: float,
453
+ line_spacing: float,
454
+ paragraph: il_version_1.PdfParagraph,
455
+ ) -> tuple[list[TypesettingUnit], bool]:
456
+ """布局排版单元。
457
+
458
+ Args:
459
+ typesetting_units: 要布局的排版单元列表
460
+ box: 布局边界框
461
+ scale: 缩放因子
462
+ line_spacing: 行间距
463
+
464
+ Returns:
465
+ tuple[list[TypesettingUnit], bool]: (已布局的排版单元列表, 是否所有单元都放得下)
466
+ """
467
+ # 计算字号众数
468
+ font_sizes = []
469
+ for unit in typesetting_units:
470
+ if getattr(unit, "font_size", None):
471
+ font_sizes.append(unit.font_size)
472
+ if getattr(unit, "char", None):
473
+ font_sizes.append(unit.char.pdf_style.font_size)
474
+ font_sizes.sort()
475
+ font_size = statistics.mode(font_sizes)
476
+
477
+ space_width = (
478
+ self.font_mapper.base_font.char_lengths("你", font_size * scale)[0] * 0.5
479
+ )
480
+
481
+ # 计算平均行高
482
+ avg_height = (
483
+ sum(unit.height * scale for unit in typesetting_units)
484
+ / len(typesetting_units)
485
+ if typesetting_units
486
+ else 0
487
+ )
488
+
489
+ # 初始化位置为右上角,并减去一个平均行高
490
+ current_x = box.x
491
+ current_y = box.y2 - avg_height
492
+ line_height = 0
493
+
494
+ # 存储已排版的单元
495
+ typeset_units = []
496
+ all_units_fit = True
497
+ last_unit: TypesettingUnit | None = None
498
+
499
+ if paragraph.first_line_indent:
500
+ current_x += space_width * 4
501
+ # 遍历所有排版单元
502
+ for unit in typesetting_units:
503
+ # 计算当前单元在当前缩放下的尺寸
504
+ unit_width = unit.width * scale
505
+ unit_height = unit.height * scale
506
+
507
+ # 跳过行首的空格
508
+ if current_x == box.x and unit.is_space:
509
+ continue
510
+
511
+ if (
512
+ last_unit # 有上一个单元
513
+ and last_unit.is_chinese_char ^ unit.is_chinese_char # 中英文交界处
514
+ and (
515
+ last_unit.box
516
+ and last_unit.box.y
517
+ and current_y - 0.1
518
+ <= last_unit.box.y2
519
+ <= current_y + line_height + 0.1
520
+ ) # 在同一行,且有垂直重叠
521
+ and not last_unit.mixed_character_blacklist # 不是混排空格黑名单字符
522
+ and not unit.mixed_character_blacklist # 同上
523
+ and current_x > box.x # 不是行首
524
+ and unit.try_get_unicode() != " " # 不是空格
525
+ and last_unit.try_get_unicode() != " " # 不是空格
526
+ ):
527
+ current_x += space_width * 0.5
528
+
529
+ # 如果当前行放不下这个元素,换行
530
+ if current_x + unit_width > box.x2 and not unit.is_hung_punctuation:
531
+ # 换行
532
+ current_x = box.x
533
+ current_y -= line_height * line_spacing
534
+ line_height = 0.0
535
+
536
+ # 检查是否超出底部边界
537
+ if current_y - unit_height < box.y:
538
+ all_units_fit = False
539
+ break
540
+
541
+ if unit.is_space:
542
+ line_height = max(line_height, unit_height)
543
+ continue
544
+
545
+ # 放置当前单元
546
+ relocated_unit = unit.relocate(current_x, current_y, scale)
547
+ typeset_units.append(relocated_unit)
548
+
549
+ # workaround: 超长行距暂时没找到具体原因,有待进一步修复。这里的1.2是魔法数字!
550
+ # 更新当前行的最大高度
551
+ if line_height == 0 or line_height * 1.2 > unit_height > line_height:
552
+ line_height = unit_height
553
+
554
+ # 更新 x 坐标
555
+ current_x = relocated_unit.box.x2
556
+
557
+ last_unit = relocated_unit
558
+
559
+ return typeset_units, all_units_fit
560
+
561
+ def retypeset(
562
+ self,
563
+ paragraph: il_version_1.PdfParagraph,
564
+ page: il_version_1.Page,
565
+ typesetting_units: list[TypesettingUnit],
566
+ ):
567
+ box = paragraph.box
568
+ scale = 1.0
569
+ line_spacing = 1.5 # 初始行距为1.7
570
+ min_scale = 0.1 # 最小缩放因子
571
+ min_line_spacing = 1.4 # 最小行距
572
+ expand_space_flag = False
573
+
574
+ while scale >= min_scale:
575
+ # 尝试布局排版单元
576
+ typeset_units, all_units_fit = self._layout_typesetting_units(
577
+ typesetting_units,
578
+ box,
579
+ scale,
580
+ line_spacing,
581
+ paragraph,
582
+ )
583
+
584
+ # 如果所有单元都放得下,就完成排版
585
+ if all_units_fit:
586
+ # 将排版后的单元转换为段落组合
587
+ paragraph.scale = scale
588
+ paragraph.pdf_paragraph_composition = []
589
+ for unit in typeset_units:
590
+ for char in unit.render():
591
+ paragraph.pdf_paragraph_composition.append(
592
+ PdfParagraphComposition(pdf_character=char),
593
+ )
594
+ return
595
+
596
+ if not expand_space_flag:
597
+ # 如果尚未扩展空格,进行扩展
598
+ max_x = self.get_max_right_space(box, page)
599
+ # 只有当有额外空间时才扩展
600
+ if max_x > box.x2:
601
+ expanded_box = Box(
602
+ x=box.x,
603
+ y=box.y,
604
+ x2=max_x, # 直接扩展到最大可用位置
605
+ y2=box.y2,
606
+ )
607
+ # 更新段落的边界框
608
+ paragraph.box = expanded_box
609
+ expand_space_flag = True
610
+ continue
611
+
612
+ # 如果当前行距大于最小行距,先减小行距
613
+ if line_spacing > min_line_spacing:
614
+ line_spacing -= 0.1
615
+ else:
616
+ # 行距已经最小,减小缩放因子
617
+ if scale > 0.6:
618
+ scale -= 0.05
619
+ else:
620
+ scale -= 0.1
621
+ line_spacing = 1.5 # 重置行距
622
+
623
+ if scale < 0.7 and min_line_spacing > 1.1:
624
+ min_line_spacing = 1.1
625
+ scale = 1.0
626
+ line_spacing = 1.5
627
+
628
+ def create_typesetting_units(
629
+ self,
630
+ paragraph: il_version_1.PdfParagraph,
631
+ fonts: dict[str, il_version_1.PdfFont],
632
+ ) -> list[TypesettingUnit]:
633
+ if not paragraph.pdf_paragraph_composition:
634
+ return []
635
+ result = []
636
+
637
+ @cache
638
+ def get_font(font_id: str, xobj_id: int):
639
+ if xobj_id in fonts:
640
+ font = fonts[xobj_id][font_id]
641
+ else:
642
+ font = fonts[font_id]
643
+ return font
644
+
645
+ for composition in paragraph.pdf_paragraph_composition:
646
+ if composition is None:
647
+ continue
648
+ if composition.pdf_line:
649
+ result.extend(
650
+ [
651
+ TypesettingUnit(char=char)
652
+ for char in composition.pdf_line.pdf_character
653
+ ],
654
+ )
655
+ elif composition.pdf_character:
656
+ result.append(
657
+ TypesettingUnit(
658
+ char=composition.pdf_character,
659
+ debug_info=paragraph.debug_info,
660
+ ),
661
+ )
662
+ elif composition.pdf_same_style_characters:
663
+ result.extend(
664
+ [
665
+ TypesettingUnit(char=char)
666
+ for char in composition.pdf_same_style_characters.pdf_character
667
+ ],
668
+ )
669
+ elif composition.pdf_same_style_unicode_characters:
670
+ font_id = (
671
+ composition.pdf_same_style_unicode_characters.pdf_style.font_id
672
+ )
673
+ font = get_font(font_id, paragraph.xobj_id)
674
+ result.extend(
675
+ [
676
+ TypesettingUnit(
677
+ unicode=char_unicode,
678
+ font=self.font_mapper.map(
679
+ font,
680
+ char_unicode,
681
+ ),
682
+ font_size=composition.pdf_same_style_unicode_characters.pdf_style.font_size,
683
+ style=composition.pdf_same_style_unicode_characters.pdf_style,
684
+ xobj_id=paragraph.xobj_id,
685
+ debug_info=composition.pdf_same_style_unicode_characters.debug_info,
686
+ )
687
+ for char_unicode in composition.pdf_same_style_unicode_characters.unicode
688
+ if char_unicode not in ("\n",)
689
+ ],
690
+ )
691
+ elif composition.pdf_formula:
692
+ result.extend([TypesettingUnit(formular=composition.pdf_formula)])
693
+ else:
694
+ logger.error(
695
+ f"Unknown composition type. "
696
+ f"Composition: {composition}. "
697
+ f"Paragraph: {paragraph}. ",
698
+ )
699
+ continue
700
+ result = list(
701
+ filter(
702
+ lambda x: x.unicode is None or x.font is not None,
703
+ result,
704
+ ),
705
+ )
706
+ return result
707
+
708
+ def create_passthrough_composition(
709
+ self,
710
+ typesetting_units: list[TypesettingUnit],
711
+ ) -> list[PdfParagraphComposition]:
712
+ """从排版单元创建直接传递的段落组合。
713
+
714
+ Args:
715
+ typesetting_units: 排版单元列表
716
+
717
+ Returns:
718
+ 段落组合列表
719
+ """
720
+ composition = []
721
+ for unit in typesetting_units:
722
+ composition.extend(
723
+ [
724
+ PdfParagraphComposition(pdf_character=char)
725
+ for char in unit.passthrough()
726
+ ],
727
+ )
728
+ return composition
729
+
730
+ def get_max_right_space(self, current_box: Box, page) -> float:
731
+ """获取段落右侧最大可用空间
732
+
733
+ Args:
734
+ current_box: 当前段落的边界框
735
+ page: 当前页面
736
+
737
+ Returns:
738
+ 可以扩展到的最大 x 坐标
739
+ """
740
+ # TODO: try to find right margin of page
741
+ # 获取页面的裁剪框作为初始最大限制
742
+ max_x = page.cropbox.box.x2 * 0.9
743
+
744
+ # 检查所有可能的阻挡元素
745
+ for para in page.pdf_paragraph:
746
+ if para.box == current_box or para.box is None: # 跳过当前段落
747
+ continue
748
+ # 只考虑在当前段落右侧且有垂直重叠的元素
749
+ if para.box.x > current_box.x and not (
750
+ para.box.y >= current_box.y2 or para.box.y2 <= current_box.y
751
+ ):
752
+ max_x = min(max_x, para.box.x)
753
+
754
+ # 检查图形
755
+ for figure in page.pdf_figure:
756
+ if figure.box.x > current_box.x and not (
757
+ figure.box.y >= current_box.y2 or figure.box.y2 <= current_box.y
758
+ ):
759
+ max_x = min(max_x, figure.box.x)
760
+
761
+ return max_x