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,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
|