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,701 @@
1
+ import base64
2
+ import math
3
+ import re
4
+ import unicodedata
5
+
6
+ from babeldoc.document_il.il_version_1 import Box
7
+ from babeldoc.document_il.il_version_1 import Document
8
+ from babeldoc.document_il.il_version_1 import GraphicState
9
+ from babeldoc.document_il.il_version_1 import Page
10
+ from babeldoc.document_il.il_version_1 import PdfCharacter
11
+ from babeldoc.document_il.il_version_1 import PdfFormula
12
+ from babeldoc.document_il.il_version_1 import PdfLine
13
+ from babeldoc.document_il.il_version_1 import PdfParagraphComposition
14
+ from babeldoc.document_il.il_version_1 import PdfSameStyleCharacters
15
+ from babeldoc.document_il.il_version_1 import PdfStyle
16
+ from babeldoc.document_il.utils.fontmap import FontMapper
17
+ from babeldoc.document_il.utils.layout_helper import LEFT_BRACKET
18
+ from babeldoc.document_il.utils.layout_helper import RIGHT_BRACKET
19
+ from babeldoc.document_il.utils.layout_helper import formular_height_ignore_char
20
+ from babeldoc.document_il.utils.layout_helper import is_same_style
21
+ from babeldoc.translation_config import TranslationConfig
22
+
23
+
24
+ class StylesAndFormulas:
25
+ stage_name = "解析公式与样式"
26
+
27
+ def __init__(self, translation_config: TranslationConfig):
28
+ self.translation_config = translation_config
29
+ self.font_mapper = FontMapper(translation_config)
30
+
31
+ def process(self, document: Document):
32
+ with self.translation_config.progress_monitor.stage_start(
33
+ self.stage_name,
34
+ len(document.page),
35
+ ) as pbar:
36
+ for page in document.page:
37
+ self.translation_config.raise_if_cancelled()
38
+ self.process_page(page)
39
+ pbar.advance()
40
+
41
+ def process_page(self, page: Page):
42
+ """处理页面,包括公式识别和偏移量计算"""
43
+ self.process_page_formulas(page)
44
+ self.process_page_offsets(page)
45
+ self.process_comma_formulas(page)
46
+ self.merge_overlapping_formulas(page)
47
+ self.process_page_offsets(page)
48
+ self.process_translatable_formulas(page)
49
+ self.process_page_styles(page)
50
+
51
+ def update_line_data(self, line: PdfLine):
52
+ min_x = min(char.box.x for char in line.pdf_character)
53
+ min_y = min(char.box.y for char in line.pdf_character)
54
+ max_x = max(char.box.x2 for char in line.pdf_character)
55
+ max_y = max(char.box.y2 for char in line.pdf_character)
56
+ line.box = Box(min_x, min_y, max_x, max_y)
57
+
58
+ def process_page_formulas(self, page: Page):
59
+ if not page.pdf_paragraph:
60
+ return
61
+
62
+ # 收集该页所有的公式字体ID
63
+ formula_font_ids = set()
64
+ for font in page.pdf_font:
65
+ if self.is_formulas_font(font.name):
66
+ formula_font_ids.add(font.font_id)
67
+
68
+ for paragraph in page.pdf_paragraph:
69
+ if not paragraph.pdf_paragraph_composition:
70
+ continue
71
+
72
+ new_compositions = []
73
+
74
+ for composition in paragraph.pdf_paragraph_composition:
75
+ current_chars = []
76
+ in_formula_state = False # 当前是否在处理公式字符
77
+ in_corner_mark_state = False
78
+
79
+ line = composition.pdf_line
80
+ if not line:
81
+ new_compositions.append(composition)
82
+ continue
83
+ for char in line.pdf_character:
84
+ is_formula = (
85
+ ( # 区分公式开头的字符&公式中间的字符。主要是逗号不能在公式开头,但是可以在中间。
86
+ (
87
+ self.is_formulas_start_char(char.char_unicode)
88
+ and not in_formula_state
89
+ )
90
+ or (
91
+ self.is_formulas_middle_char(char.char_unicode)
92
+ and in_formula_state
93
+ )
94
+ ) # 公式字符
95
+ or char.pdf_style.font_id in formula_font_ids # 公式字体
96
+ or char.vertical # 垂直字体
97
+ or (
98
+ # 如果是程序添加的dummy空格
99
+ char.char_unicode is None and in_formula_state
100
+ )
101
+ )
102
+
103
+ # isspace = get_char_unicode_string(current_chars).isspace()
104
+ isspace = all(x.char_unicode.isspace() for x in current_chars)
105
+ is_corner_mark = (
106
+ len(current_chars) > 0
107
+ and not isspace
108
+ # 角标字体,有 0.76 的角标和 0.799 的大写,这里用 0.79 取中,同时考虑首字母放大的情况
109
+ and char.pdf_style.font_size
110
+ < current_chars[-1].pdf_style.font_size * 0.79
111
+ and not in_corner_mark_state
112
+ ) or (
113
+ len(current_chars) > 0
114
+ and not isspace
115
+ # 角标字体,有 0.76 的角标和 0.799 的大写,这里用 0.79 取中,同时考虑首字母放大的情况
116
+ and char.pdf_style.font_size
117
+ < current_chars[-1].pdf_style.font_size * 1.1
118
+ and in_corner_mark_state
119
+ )
120
+
121
+ is_formula = is_formula or is_corner_mark
122
+
123
+ if char.char_unicode == " ":
124
+ is_formula = in_formula_state
125
+
126
+ if is_formula != in_formula_state and current_chars:
127
+ # 字符类型发生切换,处理之前的字符
128
+ new_compositions.append(
129
+ self.create_composition(current_chars, in_formula_state),
130
+ )
131
+ current_chars = []
132
+ in_formula_state = is_formula
133
+ in_corner_mark_state = is_corner_mark
134
+
135
+ current_chars.append(char)
136
+
137
+ # 处理行末的字符
138
+ if current_chars:
139
+ new_compositions.append(
140
+ self.create_composition(current_chars, in_formula_state),
141
+ )
142
+ current_chars = []
143
+
144
+ paragraph.pdf_paragraph_composition = new_compositions
145
+
146
+ def process_translatable_formulas(self, page: Page):
147
+ """将需要正常翻译的公式(如纯数字、数字加逗号等)转换为普通文本行"""
148
+ if not page.pdf_paragraph:
149
+ return
150
+
151
+ for paragraph in page.pdf_paragraph:
152
+ if not paragraph.pdf_paragraph_composition:
153
+ continue
154
+
155
+ new_compositions = []
156
+ for composition in paragraph.pdf_paragraph_composition:
157
+ if composition.pdf_formula is not None and self.is_translatable_formula(
158
+ composition.pdf_formula,
159
+ ):
160
+ # 将可翻译公式转换为普通文本行
161
+ new_line = PdfLine(
162
+ pdf_character=composition.pdf_formula.pdf_character,
163
+ )
164
+ self.update_line_data(new_line)
165
+ new_compositions.append(PdfParagraphComposition(pdf_line=new_line))
166
+ else:
167
+ new_compositions.append(composition)
168
+
169
+ paragraph.pdf_paragraph_composition = new_compositions
170
+
171
+ def process_page_styles(self, page: Page):
172
+ """处理页面中的文本样式,识别相同样式的文本"""
173
+ if not page.pdf_paragraph:
174
+ return
175
+
176
+ for paragraph in page.pdf_paragraph:
177
+ if not paragraph.pdf_paragraph_composition:
178
+ continue
179
+
180
+ # 计算基准样式(除公式外所有文字样式的交集)
181
+ base_style = self._calculate_base_style(paragraph)
182
+ paragraph.pdf_style = base_style
183
+
184
+ # 重新组织段落中的文本,将相同样式的文本组合在一起
185
+ new_compositions = []
186
+ current_chars = []
187
+ current_style = None
188
+
189
+ for comp in paragraph.pdf_paragraph_composition:
190
+ if comp.pdf_formula is not None:
191
+ if current_chars:
192
+ new_comp = self._create_same_style_composition(
193
+ current_chars,
194
+ current_style,
195
+ )
196
+ new_compositions.append(new_comp)
197
+ current_chars = []
198
+ new_compositions.append(comp)
199
+ continue
200
+
201
+ if not comp.pdf_line:
202
+ new_compositions.append(comp)
203
+ continue
204
+
205
+ for char in comp.pdf_line.pdf_character:
206
+ char_style = char.pdf_style
207
+ if current_style is None:
208
+ current_style = char_style
209
+ current_chars.append(char)
210
+ elif is_same_style(char_style, current_style):
211
+ current_chars.append(char)
212
+ else:
213
+ if current_chars:
214
+ new_comp = self._create_same_style_composition(
215
+ current_chars,
216
+ current_style,
217
+ )
218
+ new_compositions.append(new_comp)
219
+ current_chars = [char]
220
+ current_style = char_style
221
+
222
+ if current_chars:
223
+ new_comp = self._create_same_style_composition(
224
+ current_chars,
225
+ current_style,
226
+ )
227
+ new_compositions.append(new_comp)
228
+
229
+ paragraph.pdf_paragraph_composition = new_compositions
230
+
231
+ def _calculate_base_style(self, paragraph) -> PdfStyle:
232
+ """计算段落的基准样式(除公式外所有文字样式的交集)"""
233
+ styles = []
234
+ for comp in paragraph.pdf_paragraph_composition:
235
+ if isinstance(comp, PdfFormula):
236
+ continue
237
+ if not comp.pdf_line:
238
+ continue
239
+ for char in comp.pdf_line.pdf_character:
240
+ styles.append(char.pdf_style)
241
+
242
+ if not styles:
243
+ return None
244
+
245
+ # 返回所有样式的交集
246
+ base_style = styles[0]
247
+ for style in styles[1:]:
248
+ # 更新基准样式为所有样式的交集
249
+ base_style = self._merge_styles(base_style, style)
250
+
251
+ # 如果font_id或font_size为None,则使用众数
252
+ if base_style.font_id is None:
253
+ base_style.font_id = self._get_mode_value([s.font_id for s in styles])
254
+ if base_style.font_size is None:
255
+ base_style.font_size = self._get_mode_value([s.font_size for s in styles])
256
+
257
+ return base_style
258
+
259
+ def _get_mode_value(self, values):
260
+ """计算列表中的众数"""
261
+ if not values:
262
+ return None
263
+ from collections import Counter
264
+
265
+ counter = Counter(values)
266
+ return counter.most_common(1)[0][0]
267
+
268
+ def _merge_styles(self, style1, style2):
269
+ """合并两个样式,返回它们的交集"""
270
+ if style1 is None or style1.font_size is None:
271
+ return style2
272
+ if style2 is None or style2.font_size is None:
273
+ return style1
274
+
275
+ return PdfStyle(
276
+ font_id=style1.font_id if style1.font_id == style2.font_id else None,
277
+ font_size=(
278
+ style1.font_size
279
+ if math.fabs(style1.font_size - style2.font_size) < 0.02
280
+ else None
281
+ ),
282
+ graphic_state=self._merge_graphic_states(
283
+ style1.graphic_state,
284
+ style2.graphic_state,
285
+ ),
286
+ )
287
+
288
+ def _merge_graphic_states(self, state1, state2):
289
+ """合并两个GraphicState,返回它们的交集"""
290
+ if state1 is None:
291
+ return state2
292
+ if state2 is None:
293
+ return state1
294
+
295
+ return GraphicState(
296
+ linewidth=(
297
+ state1.linewidth if state1.linewidth == state2.linewidth else None
298
+ ),
299
+ dash=state1.dash if state1.dash == state2.dash else None,
300
+ flatness=state1.flatness if state1.flatness == state2.flatness else None,
301
+ intent=state1.intent if state1.intent == state2.intent else None,
302
+ linecap=state1.linecap if state1.linecap == state2.linecap else None,
303
+ linejoin=state1.linejoin if state1.linejoin == state2.linejoin else None,
304
+ miterlimit=(
305
+ state1.miterlimit if state1.miterlimit == state2.miterlimit else None
306
+ ),
307
+ ncolor=state1.ncolor if state1.ncolor == state2.ncolor else None,
308
+ scolor=state1.scolor if state1.scolor == state2.scolor else None,
309
+ stroking_color_space_name=(
310
+ state1.stroking_color_space_name
311
+ if state1.stroking_color_space_name == state2.stroking_color_space_name
312
+ else None
313
+ ),
314
+ non_stroking_color_space_name=(
315
+ state1.non_stroking_color_space_name
316
+ if state1.non_stroking_color_space_name
317
+ == state2.non_stroking_color_space_name
318
+ else None
319
+ ),
320
+ passthrough_per_char_instruction=(
321
+ state1.passthrough_per_char_instruction
322
+ if state1.passthrough_per_char_instruction
323
+ == state2.passthrough_per_char_instruction
324
+ else None
325
+ ),
326
+ )
327
+
328
+ def _create_same_style_composition(
329
+ self,
330
+ chars: list[PdfCharacter],
331
+ style,
332
+ ) -> PdfParagraphComposition:
333
+ """创建具有相同样式的文本组合"""
334
+ if not chars:
335
+ return None
336
+
337
+ # 计算边界框
338
+ min_x = min(char.box.x for char in chars)
339
+ min_y = min(char.box.y for char in chars)
340
+ max_x = max(char.box.x2 for char in chars)
341
+ max_y = max(char.box.y2 for char in chars)
342
+ box = Box(min_x, min_y, max_x, max_y)
343
+
344
+ return PdfParagraphComposition(
345
+ pdf_same_style_characters=PdfSameStyleCharacters(
346
+ box=box,
347
+ pdf_style=style,
348
+ pdf_character=chars,
349
+ ),
350
+ )
351
+
352
+ def process_page_offsets(self, page: Page):
353
+ """计算公式的x和y偏移量"""
354
+ if not page.pdf_paragraph:
355
+ return
356
+
357
+ for paragraph in page.pdf_paragraph:
358
+ if not paragraph.pdf_paragraph_composition:
359
+ continue
360
+
361
+ # 计算该段落的行间距,用其80%作为容差
362
+ line_spacing = self.calculate_line_spacing(paragraph)
363
+ y_tolerance = line_spacing * 0.8
364
+
365
+ for i, composition in enumerate(paragraph.pdf_paragraph_composition):
366
+ if not composition.pdf_formula:
367
+ continue
368
+
369
+ formula = composition.pdf_formula
370
+ left_line = None
371
+ right_line = None
372
+
373
+ # 查找左边最近的同一行的文本
374
+ for j in range(i - 1, -1, -1):
375
+ comp = paragraph.pdf_paragraph_composition[j]
376
+ if comp.pdf_line:
377
+ # 检查y坐标是否接近,判断是否在同一行
378
+ if abs(comp.pdf_line.box.y - formula.box.y) <= y_tolerance:
379
+ left_line = comp.pdf_line
380
+ break
381
+
382
+ # 查找右边最近的同一行的文本
383
+ for j in range(i + 1, len(paragraph.pdf_paragraph_composition)):
384
+ comp = paragraph.pdf_paragraph_composition[j]
385
+ if comp.pdf_line:
386
+ # 检查y坐标是否接近,判断是否在同一行
387
+ if abs(comp.pdf_line.box.y - formula.box.y) <= y_tolerance:
388
+ right_line = comp.pdf_line
389
+ break
390
+
391
+ # 计算x偏移量(相对于左边文本)
392
+ if left_line:
393
+ formula.x_offset = formula.box.x - left_line.box.x2
394
+ else:
395
+ formula.x_offset = 0 # 如果左边没有文字,x_offset应该为0
396
+ if abs(formula.x_offset) < 0.1:
397
+ formula.x_offset = 0
398
+ if formula.x_offset > 0:
399
+ formula.x_offset = 0
400
+
401
+ # 计算y偏移量
402
+ if left_line:
403
+ # 使用底部坐标计算偏移量
404
+ formula.y_offset = formula.box.y - left_line.box.y
405
+ elif right_line:
406
+ formula.y_offset = formula.box.y - right_line.box.y
407
+ else:
408
+ formula.y_offset = 0
409
+
410
+ if abs(formula.y_offset) < 0.1:
411
+ formula.y_offset = 0
412
+
413
+ def calculate_line_spacing(self, paragraph) -> float:
414
+ """计算段落中的平均行间距"""
415
+ if not paragraph.pdf_paragraph_composition:
416
+ return 0.0
417
+
418
+ # 收集所有文本行的y坐标
419
+ line_y_positions = []
420
+ for comp in paragraph.pdf_paragraph_composition:
421
+ if comp.pdf_line:
422
+ line_y_positions.append(comp.pdf_line.box.y)
423
+
424
+ if len(line_y_positions) < 2:
425
+ return 10.0 # 如果只有一行或没有行,返回一个默认值
426
+
427
+ # 计算相邻行之间的y差值
428
+ line_spacings = []
429
+ for i in range(len(line_y_positions) - 1):
430
+ spacing = abs(line_y_positions[i] - line_y_positions[i + 1])
431
+ if spacing > 0: # 忽略重叠的行
432
+ line_spacings.append(spacing)
433
+
434
+ if not line_spacings:
435
+ return 10.0 # 如果没有有效的行间距,返回默认值
436
+
437
+ # 使用中位数来避免异常值的影响
438
+ median_spacing = sorted(line_spacings)[len(line_spacings) // 2]
439
+ return median_spacing
440
+
441
+ def create_composition(
442
+ self,
443
+ chars: list[PdfCharacter],
444
+ is_formula: bool,
445
+ ) -> PdfParagraphComposition:
446
+ if is_formula:
447
+ formula = PdfFormula(pdf_character=chars)
448
+ self.update_formula_data(formula)
449
+ return PdfParagraphComposition(pdf_formula=formula)
450
+ else:
451
+ new_line = PdfLine(pdf_character=chars)
452
+ self.update_line_data(new_line)
453
+ return PdfParagraphComposition(pdf_line=new_line)
454
+
455
+ def update_formula_data(self, formula: PdfFormula):
456
+ min_x = min(char.box.x for char in formula.pdf_character)
457
+ max_x = max(char.box.x2 for char in formula.pdf_character)
458
+ if not all(map(formular_height_ignore_char, formula.pdf_character)):
459
+ min_y = min(
460
+ char.box.y
461
+ for char in formula.pdf_character
462
+ if not formular_height_ignore_char(char)
463
+ )
464
+ max_y = max(
465
+ char.box.y2
466
+ for char in formula.pdf_character
467
+ if not formular_height_ignore_char(char)
468
+ )
469
+ else:
470
+ min_y = min(char.box.y for char in formula.pdf_character)
471
+ max_y = max(char.box.y2 for char in formula.pdf_character)
472
+ formula.box = Box(min_x, min_y, max_x, max_y)
473
+
474
+ def is_translatable_formula(self, formula: PdfFormula) -> bool:
475
+ """判断公式是否只包含需要正常翻译的字符(数字、空格和英文逗号)"""
476
+ text = "".join(char.char_unicode for char in formula.pdf_character)
477
+ if formula.y_offset > 0.1:
478
+ return False
479
+ return bool(re.match(r"^[0-9, ]+$", text))
480
+
481
+ def is_formulas_font(self, font_name: str) -> bool:
482
+ pattern2 = r"^(Cambria|Cambria-BoldItalic|Cambria-Bold|Cambria-Italic)$"
483
+ if self.translation_config.formular_font_pattern:
484
+ pattern = self.translation_config.formular_font_pattern
485
+ else:
486
+ pattern = (
487
+ r"(CM[^RB]"
488
+ r"|(MS|XY|MT|BL|RM|EU|LA|RS)[A-Z]"
489
+ r"|LINE"
490
+ r"|LCIRCLE"
491
+ r"|TeX-"
492
+ r"|rsfs"
493
+ r"|txsy"
494
+ r"|wasy"
495
+ r"|stmary"
496
+ r"|.*Mono"
497
+ r"|.*Code"
498
+ r"|.*Ital"
499
+ r"|.*Sym"
500
+ r"|.*Math"
501
+ r")"
502
+ )
503
+
504
+ if font_name.startswith("BASE64:"):
505
+ font_name_bytes = base64.b64decode(font_name[7:])
506
+ font = font_name_bytes.split(b"+")[-1]
507
+ pattern2 = pattern2.encode()
508
+ pattern = pattern.encode()
509
+ else:
510
+ font = font_name.split("+")[-1]
511
+
512
+ if re.match(pattern2, font):
513
+ return False
514
+ if re.match(pattern, font):
515
+ return True
516
+
517
+ return False
518
+
519
+ def is_formulas_start_char(self, char: str) -> bool:
520
+ if "(cid:" in char:
521
+ return True
522
+ if not self.font_mapper.has_char(char):
523
+ return True
524
+ if self.translation_config.formular_char_pattern:
525
+ pattern = self.translation_config.formular_char_pattern
526
+ if re.match(pattern, char):
527
+ return True
528
+ if (
529
+ char
530
+ and char != " " # 非空格
531
+ and (
532
+ unicodedata.category(char[0])
533
+ in [
534
+ "Lm",
535
+ "Mn",
536
+ "Sk",
537
+ "Sm",
538
+ "Zl",
539
+ "Zp",
540
+ "Zs",
541
+ "Co", # private use character
542
+ "So", # symbol
543
+ ] # 文字修饰符、数学符号、分隔符号
544
+ or ord(char[0]) in range(0x370, 0x400) # 希腊字母
545
+ )
546
+ ):
547
+ return True
548
+ if re.match("[0-9\\[\\]•]", char):
549
+ return True
550
+ return False
551
+
552
+ def is_formulas_middle_char(self, char: str) -> bool:
553
+ if self.is_formulas_start_char(char):
554
+ return True
555
+
556
+ if re.match(",", char):
557
+ return True
558
+
559
+ def should_split_formula(self, formula: PdfFormula) -> bool:
560
+ """判断公式是否需要按逗号拆分(包含逗号且有其他特殊符号)"""
561
+ text = "".join(char.char_unicode for char in formula.pdf_character)
562
+ # 必须包含逗号
563
+ if "," not in text:
564
+ return False
565
+ # 检查是否包含除了数字和[]之外的其他符号
566
+ text_without_basic = re.sub(r"[0-9\[\],\s]", "", text)
567
+ return bool(text_without_basic)
568
+
569
+ def split_formula_by_comma(
570
+ self,
571
+ formula: PdfFormula,
572
+ ) -> list[tuple[list[PdfCharacter], PdfCharacter]]:
573
+ """按逗号拆分公式字符,返回(字符组, 逗号字符)的列表,最后一组的逗号字符为None。
574
+ 只有不在括号内的逗号才会被用作分隔符。支持的括号对包括:
575
+ - (cid:8) 和 (cid:9)
576
+ - ( 和 )
577
+ - (cid:16) 和 (cid:17)
578
+ """
579
+ result = []
580
+ current_chars = []
581
+ bracket_level = 0 # 跟踪括号的层数
582
+
583
+ for char in formula.pdf_character:
584
+ # 检查是否是左括号
585
+ if char.char_unicode in LEFT_BRACKET:
586
+ bracket_level += 1
587
+ current_chars.append(char)
588
+ # 检查是否是右括号
589
+ elif char.char_unicode in RIGHT_BRACKET:
590
+ bracket_level = max(0, bracket_level - 1) # 防止括号不匹配的情况
591
+ current_chars.append(char)
592
+ # 检查是否是逗号,且不在括号内
593
+ elif char.char_unicode == "," and bracket_level == 0:
594
+ if current_chars:
595
+ result.append((current_chars, char))
596
+ current_chars = []
597
+ else:
598
+ current_chars.append(char)
599
+
600
+ if current_chars:
601
+ result.append((current_chars, None)) # 最后一组没有逗号
602
+
603
+ return result
604
+
605
+ def merge_formulas(self, formula1: PdfFormula, formula2: PdfFormula) -> PdfFormula:
606
+ """合并两个公式,保持字符的相对位置"""
607
+ # 合并所有字符
608
+ all_chars = formula1.pdf_character + formula2.pdf_character
609
+ # 按y坐标和x坐标排序,确保字符顺序正确
610
+ sorted_chars = sorted(all_chars, key=lambda c: (c.box.y, c.box.x))
611
+
612
+ merged_formula = PdfFormula(pdf_character=sorted_chars)
613
+ self.update_formula_data(merged_formula)
614
+ return merged_formula
615
+
616
+ def merge_overlapping_formulas(self, page: Page):
617
+ """
618
+ 合并x轴重叠且y轴有交集的相邻公式
619
+ 角标可能会被识别成单独的公式,需要合并
620
+ """
621
+ if not page.pdf_paragraph:
622
+ return
623
+
624
+ for paragraph in page.pdf_paragraph:
625
+ if not paragraph.pdf_paragraph_composition:
626
+ continue
627
+
628
+ i = 0
629
+ while i < len(paragraph.pdf_paragraph_composition) - 1:
630
+ comp1 = paragraph.pdf_paragraph_composition[i]
631
+ comp2 = paragraph.pdf_paragraph_composition[i + 1]
632
+
633
+ # 检查是否都是公式
634
+ if comp1.pdf_formula is None or comp2.pdf_formula is None:
635
+ i += 1
636
+ continue
637
+
638
+ formula1 = comp1.pdf_formula
639
+ formula2 = comp2.pdf_formula
640
+
641
+ # 检查x轴重叠和y轴交集
642
+ if self.is_x_axis_contained(
643
+ formula1.box,
644
+ formula2.box,
645
+ ) and self.has_y_intersection(formula1.box, formula2.box):
646
+ # 合并公式
647
+ merged_formula = self.merge_formulas(formula1, formula2)
648
+ paragraph.pdf_paragraph_composition[i] = PdfParagraphComposition(
649
+ pdf_formula=merged_formula,
650
+ )
651
+ # 删除第二个公式
652
+ del paragraph.pdf_paragraph_composition[i + 1]
653
+ # 不增加i,因为合并后的公式可能还需要和下一个公式合并
654
+ else:
655
+ i += 1
656
+
657
+ def is_x_axis_contained(self, box1: Box, box2: Box) -> bool:
658
+ """判断box1的x轴是否完全包含在box2的x轴内,或反之"""
659
+ return (box1.x >= box2.x and box1.x2 <= box2.x2) or (
660
+ box2.x >= box1.x and box2.x2 <= box1.x2
661
+ )
662
+
663
+ def has_y_intersection(self, box1: Box, box2: Box) -> bool:
664
+ """判断两个box的y轴是否有交集"""
665
+ return not (box1.y2 < box2.y or box2.y2 < box1.y)
666
+
667
+ def process_comma_formulas(self, page: Page):
668
+ """处理包含逗号的复杂公式,将其按逗号拆分"""
669
+ if not page.pdf_paragraph:
670
+ return
671
+
672
+ for paragraph in page.pdf_paragraph:
673
+ if not paragraph.pdf_paragraph_composition:
674
+ continue
675
+
676
+ new_compositions = []
677
+ for composition in paragraph.pdf_paragraph_composition:
678
+ if composition.pdf_formula is not None and self.should_split_formula(
679
+ composition.pdf_formula,
680
+ ):
681
+ # 按逗号拆分公式
682
+ char_groups = self.split_formula_by_comma(composition.pdf_formula)
683
+ for chars, comma in char_groups:
684
+ if chars: # 忽略空组(连续的逗号)
685
+ formula = PdfFormula(pdf_character=chars)
686
+ self.update_formula_data(formula)
687
+ new_compositions.append(
688
+ PdfParagraphComposition(pdf_formula=formula),
689
+ )
690
+
691
+ # 如果有逗号,添加为文本行
692
+ if comma:
693
+ comma_line = PdfLine(pdf_character=[comma])
694
+ self.update_line_data(comma_line)
695
+ new_compositions.append(
696
+ PdfParagraphComposition(pdf_line=comma_line),
697
+ )
698
+ else:
699
+ new_compositions.append(composition)
700
+
701
+ paragraph.pdf_paragraph_composition = new_compositions