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,467 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
from babeldoc.document_il import GraphicState
|
|
5
|
+
from babeldoc.document_il.il_version_1 import Box
|
|
6
|
+
from babeldoc.document_il.il_version_1 import PdfCharacter
|
|
7
|
+
from babeldoc.document_il.il_version_1 import PdfParagraph
|
|
8
|
+
from babeldoc.document_il.il_version_1 import PdfParagraphComposition
|
|
9
|
+
from pymupdf import Font
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
HEIGHT_NOT_USFUL_CHAR_IN_CHAR = (
|
|
13
|
+
"∑︁",
|
|
14
|
+
# 暂时假设cid:17和cid 16是特殊情况
|
|
15
|
+
# 来源于 arXiv:2310.18608v2 第九页公式大括号
|
|
16
|
+
"(cid:17)",
|
|
17
|
+
"(cid:16)",
|
|
18
|
+
# arXiv:2411.19509v2 第四页 []
|
|
19
|
+
"(cid:104)",
|
|
20
|
+
"(cid:105)",
|
|
21
|
+
# arXiv:2411.19509v2 第四页 公式的|竖线
|
|
22
|
+
"(cid:13)",
|
|
23
|
+
"∑︁",
|
|
24
|
+
# arXiv:2412.05265 27页 累加号
|
|
25
|
+
"(cid:88)",
|
|
26
|
+
# arXiv:2412.05265 16页 累乘号
|
|
27
|
+
"(cid:89)",
|
|
28
|
+
# arXiv:2412.05265 27页 积分
|
|
29
|
+
"(cid:90)",
|
|
30
|
+
# arXiv:2412.05265 32页 公式左右的中括号
|
|
31
|
+
"(cid:2)",
|
|
32
|
+
"(cid:3)",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
LEFT_BRACKET = ("(cid:8)", "(", "(cid:16)", "{", "[", "(cid:104)", "(cid:2)")
|
|
37
|
+
RIGHT_BRACKET = ("(cid:9)", ")", "(cid:17)", "}", "]", "(cid:105)", "(cid:3)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def formular_height_ignore_char(char: PdfCharacter):
|
|
41
|
+
return (
|
|
42
|
+
char.pdf_character_id is None
|
|
43
|
+
or char.char_unicode in HEIGHT_NOT_USFUL_CHAR_IN_CHAR
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Layout:
|
|
48
|
+
def __init__(self, layout_id, name):
|
|
49
|
+
self.id = layout_id
|
|
50
|
+
self.name = name
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def is_newline(prev_char: PdfCharacter, curr_char: PdfCharacter) -> bool:
|
|
54
|
+
# 如果没有前一个字符,不是换行
|
|
55
|
+
if prev_char is None:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# 获取两个字符的中心 y 坐标
|
|
59
|
+
# prev_y = (prev_char.box.y + prev_char.box.y2) / 2
|
|
60
|
+
# curr_y = (curr_char.box.y + curr_char.box.y2) / 2
|
|
61
|
+
|
|
62
|
+
# 如果当前字符的 y 坐标明显低于前一个字符,说明换行了
|
|
63
|
+
# 这里使用字符高度的一半作为阈值
|
|
64
|
+
char_height = max(
|
|
65
|
+
curr_char.box.y2 - curr_char.box.y,
|
|
66
|
+
prev_char.box.y2 - prev_char.box.y,
|
|
67
|
+
)
|
|
68
|
+
char_width = max(
|
|
69
|
+
curr_char.box.x2 - curr_char.box.x,
|
|
70
|
+
prev_char.box.x2 - prev_char.box.x,
|
|
71
|
+
)
|
|
72
|
+
should_new_line = (
|
|
73
|
+
curr_char.box.y2 < prev_char.box.y
|
|
74
|
+
or curr_char.box.x2 < prev_char.box.x - char_width * 10
|
|
75
|
+
)
|
|
76
|
+
if should_new_line and (
|
|
77
|
+
formular_height_ignore_char(curr_char)
|
|
78
|
+
or formular_height_ignore_char(prev_char)
|
|
79
|
+
):
|
|
80
|
+
return False
|
|
81
|
+
return should_new_line
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_paragraph_length_except(
|
|
85
|
+
paragraph: PdfParagraph,
|
|
86
|
+
except_chars: str,
|
|
87
|
+
font: Font,
|
|
88
|
+
) -> int:
|
|
89
|
+
length = 0
|
|
90
|
+
for composition in paragraph.pdf_paragraph_composition:
|
|
91
|
+
if composition.pdf_character:
|
|
92
|
+
length += (
|
|
93
|
+
composition.pdf_character[0].box.x2 - composition.pdf_character[0].box.x
|
|
94
|
+
)
|
|
95
|
+
elif composition.pdf_same_style_characters:
|
|
96
|
+
for pdf_char in composition.pdf_same_style_characters.pdf_character:
|
|
97
|
+
if pdf_char.char_unicode in except_chars:
|
|
98
|
+
continue
|
|
99
|
+
length += pdf_char.box.x2 - pdf_char.box.x
|
|
100
|
+
elif composition.pdf_same_style_unicode_characters:
|
|
101
|
+
for char_unicode in composition.pdf_same_style_unicode_characters.unicode:
|
|
102
|
+
if char_unicode in except_chars:
|
|
103
|
+
continue
|
|
104
|
+
length += font.char_lengths(
|
|
105
|
+
char_unicode,
|
|
106
|
+
composition.pdf_same_style_unicode_characters.pdf_style.font_size,
|
|
107
|
+
)[0]
|
|
108
|
+
elif composition.pdf_line:
|
|
109
|
+
for pdf_char in composition.pdf_line.pdf_character:
|
|
110
|
+
if pdf_char.char_unicode in except_chars:
|
|
111
|
+
continue
|
|
112
|
+
length += pdf_char.box.x2 - pdf_char.box.x
|
|
113
|
+
elif composition.pdf_formula:
|
|
114
|
+
length += composition.pdf_formula.box.x2 - composition.pdf_formula.box.x
|
|
115
|
+
else:
|
|
116
|
+
logger.error(
|
|
117
|
+
f"Unknown composition type. "
|
|
118
|
+
f"Composition: {composition}. "
|
|
119
|
+
f"Paragraph: {paragraph}. ",
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
return length
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_paragraph_unicode(paragraph: PdfParagraph) -> str:
|
|
126
|
+
chars = []
|
|
127
|
+
for composition in paragraph.pdf_paragraph_composition:
|
|
128
|
+
if composition.pdf_line:
|
|
129
|
+
chars.extend(composition.pdf_line.pdf_character)
|
|
130
|
+
elif composition.pdf_same_style_characters:
|
|
131
|
+
chars.extend(composition.pdf_same_style_characters.pdf_character)
|
|
132
|
+
elif composition.pdf_same_style_unicode_characters:
|
|
133
|
+
chars.extend(composition.pdf_same_style_unicode_characters.unicode)
|
|
134
|
+
elif composition.pdf_formula:
|
|
135
|
+
chars.extend(composition.pdf_formula.pdf_character)
|
|
136
|
+
elif composition.pdf_character:
|
|
137
|
+
chars.append(composition.pdf_character)
|
|
138
|
+
else:
|
|
139
|
+
logger.error(
|
|
140
|
+
f"Unknown composition type. "
|
|
141
|
+
f"Composition: {composition}. "
|
|
142
|
+
f"Paragraph: {paragraph}. ",
|
|
143
|
+
)
|
|
144
|
+
continue
|
|
145
|
+
return get_char_unicode_string(chars)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_char_unicode_string(chars: list[PdfCharacter | str]) -> str:
|
|
149
|
+
"""
|
|
150
|
+
将字符列表转换为 Unicode 字符串,根据字符间距自动插入空格。
|
|
151
|
+
有些 PDF 不会显式编码空格,这时需要根据间距自动插入空格。
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
chars: 字符列表,可以是 PdfCharacter 对象或字符串
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
str: 处理后的 Unicode 字符串
|
|
158
|
+
"""
|
|
159
|
+
# 计算字符间距的中位数
|
|
160
|
+
distances = []
|
|
161
|
+
for i in range(len(chars) - 1):
|
|
162
|
+
if not (
|
|
163
|
+
isinstance(chars[i], PdfCharacter)
|
|
164
|
+
and isinstance(chars[i + 1], PdfCharacter)
|
|
165
|
+
):
|
|
166
|
+
continue
|
|
167
|
+
distance = chars[i + 1].box.x - chars[i].box.x2
|
|
168
|
+
if distance > 1: # 只考虑正向距离
|
|
169
|
+
distances.append(distance)
|
|
170
|
+
|
|
171
|
+
# 去重后的距离
|
|
172
|
+
distinct_distances = sorted(set(distances))
|
|
173
|
+
|
|
174
|
+
if not distinct_distances:
|
|
175
|
+
median_distance = 1
|
|
176
|
+
elif len(distinct_distances) == 1:
|
|
177
|
+
median_distance = distinct_distances[0]
|
|
178
|
+
else:
|
|
179
|
+
median_distance = distinct_distances[1]
|
|
180
|
+
|
|
181
|
+
# 构建 unicode 字符串,根据间距插入空格
|
|
182
|
+
unicode_chars = []
|
|
183
|
+
for i in range(len(chars)):
|
|
184
|
+
# 如果不是字符对象,直接添加,一般来说这个时候 chars[i] 是字符串
|
|
185
|
+
if not isinstance(chars[i], PdfCharacter):
|
|
186
|
+
unicode_chars.append(chars[i])
|
|
187
|
+
continue
|
|
188
|
+
unicode_chars.append(chars[i].char_unicode)
|
|
189
|
+
|
|
190
|
+
# 如果是空格,跳过
|
|
191
|
+
if chars[i].char_unicode == " ":
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
# 如果两个字符都是 PdfCharacter,检查间距
|
|
195
|
+
if i < len(chars) - 1 and isinstance(chars[i + 1], PdfCharacter):
|
|
196
|
+
distance = chars[i + 1].box.x - chars[i].box.x2
|
|
197
|
+
if distance >= median_distance or Layout.is_newline( # 间距大于中位数
|
|
198
|
+
chars[i],
|
|
199
|
+
chars[i + 1],
|
|
200
|
+
): # 换行
|
|
201
|
+
unicode_chars.append(" ") # 添加空格
|
|
202
|
+
|
|
203
|
+
return "".join(unicode_chars)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_paragraph_max_height(paragraph: PdfParagraph) -> float:
|
|
207
|
+
"""
|
|
208
|
+
获取段落中最高的排版单元高度。
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
paragraph: PDF段落对象
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
float: 最大高度值
|
|
215
|
+
"""
|
|
216
|
+
max_height = 0.0
|
|
217
|
+
for composition in paragraph.pdf_paragraph_composition:
|
|
218
|
+
if composition is None:
|
|
219
|
+
continue
|
|
220
|
+
if composition.pdf_character:
|
|
221
|
+
char_height = (
|
|
222
|
+
composition.pdf_character[0].box.y2 - composition.pdf_character[0].box.y
|
|
223
|
+
)
|
|
224
|
+
max_height = max(max_height, char_height)
|
|
225
|
+
elif composition.pdf_same_style_characters:
|
|
226
|
+
for pdf_char in composition.pdf_same_style_characters.pdf_character:
|
|
227
|
+
char_height = pdf_char.box.y2 - pdf_char.box.y
|
|
228
|
+
max_height = max(max_height, char_height)
|
|
229
|
+
elif composition.pdf_same_style_unicode_characters:
|
|
230
|
+
# 对于纯Unicode字符,我们使用其样式中的字体大小作为高度估计
|
|
231
|
+
font_size = (
|
|
232
|
+
composition.pdf_same_style_unicode_characters.pdf_style.font_size
|
|
233
|
+
)
|
|
234
|
+
max_height = max(max_height, font_size)
|
|
235
|
+
elif composition.pdf_line:
|
|
236
|
+
for pdf_char in composition.pdf_line.pdf_character:
|
|
237
|
+
char_height = pdf_char.box.y2 - pdf_char.box.y
|
|
238
|
+
max_height = max(max_height, char_height)
|
|
239
|
+
elif composition.pdf_formula:
|
|
240
|
+
formula_height = (
|
|
241
|
+
composition.pdf_formula.box.y2 - composition.pdf_formula.box.y
|
|
242
|
+
)
|
|
243
|
+
max_height = max(max_height, formula_height)
|
|
244
|
+
else:
|
|
245
|
+
logger.error(
|
|
246
|
+
f"Unknown composition type. "
|
|
247
|
+
f"Composition: {composition}. "
|
|
248
|
+
f"Paragraph: {paragraph}. ",
|
|
249
|
+
)
|
|
250
|
+
continue
|
|
251
|
+
return max_height
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def is_same_style(style1, style2) -> bool:
|
|
255
|
+
"""判断两个样式是否相同"""
|
|
256
|
+
if style1 is None or style2 is None:
|
|
257
|
+
return style1 is style2
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
style1.font_id == style2.font_id
|
|
261
|
+
and math.fabs(style1.font_size - style2.font_size) < 0.02
|
|
262
|
+
and is_same_graphic_state(style1.graphic_state, style2.graphic_state)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def is_same_style_except_size(style1, style2) -> bool:
|
|
267
|
+
"""判断两个样式是否相同"""
|
|
268
|
+
if style1 is None or style2 is None:
|
|
269
|
+
return style1 is style2
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
style1.font_id == style2.font_id
|
|
273
|
+
and 0.7 < math.fabs(style1.font_size / style2.font_size) < 1.3
|
|
274
|
+
and is_same_graphic_state(style1.graphic_state, style2.graphic_state)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def is_same_style_except_font(style1, style2) -> bool:
|
|
279
|
+
"""判断两个样式是否相同"""
|
|
280
|
+
if style1 is None or style2 is None:
|
|
281
|
+
return style1 is style2
|
|
282
|
+
|
|
283
|
+
return math.fabs(
|
|
284
|
+
style1.font_size - style2.font_size,
|
|
285
|
+
) < 0.02 and is_same_graphic_state(style1.graphic_state, style2.graphic_state)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def is_same_graphic_state(state1: GraphicState, state2: GraphicState) -> bool:
|
|
289
|
+
"""判断两个 GraphicState 是否相同"""
|
|
290
|
+
if state1 is None or state2 is None:
|
|
291
|
+
return state1 is state2
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
state1.linewidth == state2.linewidth
|
|
295
|
+
and state1.dash == state2.dash
|
|
296
|
+
and state1.flatness == state2.flatness
|
|
297
|
+
and state1.intent == state2.intent
|
|
298
|
+
and state1.linecap == state2.linecap
|
|
299
|
+
and state1.linejoin == state2.linejoin
|
|
300
|
+
and state1.miterlimit == state2.miterlimit
|
|
301
|
+
and state1.ncolor == state2.ncolor
|
|
302
|
+
and state1.scolor == state2.scolor
|
|
303
|
+
and state1.stroking_color_space_name == state2.stroking_color_space_name
|
|
304
|
+
and state1.non_stroking_color_space_name == state2.non_stroking_color_space_name
|
|
305
|
+
and state1.passthrough_per_char_instruction
|
|
306
|
+
== state2.passthrough_per_char_instruction
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def add_space_dummy_chars(paragraph: PdfParagraph) -> None:
|
|
311
|
+
"""
|
|
312
|
+
在PDF段落中添加表示空格的dummy字符。
|
|
313
|
+
这个函数会直接修改传入的paragraph对象,在需要空格的地方添加dummy字符。
|
|
314
|
+
同时也会处理不同组成部分之间的空格。
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
paragraph: 需要处理的PDF段落对象
|
|
318
|
+
"""
|
|
319
|
+
# 首先处理每个组成部分内部的空格
|
|
320
|
+
for composition in paragraph.pdf_paragraph_composition:
|
|
321
|
+
if composition.pdf_line:
|
|
322
|
+
chars = composition.pdf_line.pdf_character
|
|
323
|
+
_add_space_dummy_chars_to_list(chars)
|
|
324
|
+
elif composition.pdf_same_style_characters:
|
|
325
|
+
chars = composition.pdf_same_style_characters.pdf_character
|
|
326
|
+
_add_space_dummy_chars_to_list(chars)
|
|
327
|
+
elif composition.pdf_same_style_unicode_characters:
|
|
328
|
+
# 对于unicode字符,不需要处理。
|
|
329
|
+
# 这种类型只会出现在翻译好的结果中
|
|
330
|
+
continue
|
|
331
|
+
elif composition.pdf_formula:
|
|
332
|
+
chars = composition.pdf_formula.pdf_character
|
|
333
|
+
_add_space_dummy_chars_to_list(chars)
|
|
334
|
+
|
|
335
|
+
# 然后处理组成部分之间的空格
|
|
336
|
+
for i in range(len(paragraph.pdf_paragraph_composition) - 1):
|
|
337
|
+
curr_comp = paragraph.pdf_paragraph_composition[i]
|
|
338
|
+
next_comp = paragraph.pdf_paragraph_composition[i + 1]
|
|
339
|
+
|
|
340
|
+
# 获取当前组成部分的最后一个字符
|
|
341
|
+
curr_last_char = _get_last_char_from_composition(curr_comp)
|
|
342
|
+
if not curr_last_char:
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
# 获取下一个组成部分的第一个字符
|
|
346
|
+
next_first_char = _get_first_char_from_composition(next_comp)
|
|
347
|
+
if not next_first_char:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
# 检查两个组成部分之间是否需要添加空格
|
|
351
|
+
distance = next_first_char.box.x - curr_last_char.box.x2
|
|
352
|
+
if distance > 1: # 只考虑正向距离
|
|
353
|
+
# 创建一个dummy字符作为空格
|
|
354
|
+
space_box = Box(
|
|
355
|
+
x=curr_last_char.box.x2,
|
|
356
|
+
y=curr_last_char.box.y,
|
|
357
|
+
x2=curr_last_char.box.x2 + distance,
|
|
358
|
+
y2=curr_last_char.box.y2,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
space_char = PdfCharacter(
|
|
362
|
+
pdf_style=curr_last_char.pdf_style,
|
|
363
|
+
box=space_box,
|
|
364
|
+
char_unicode=" ",
|
|
365
|
+
scale=curr_last_char.scale,
|
|
366
|
+
advance=space_box.x2 - space_box.x,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# 将空格添加到当前组成部分的末尾
|
|
370
|
+
if curr_comp.pdf_line:
|
|
371
|
+
curr_comp.pdf_line.pdf_character.append(space_char)
|
|
372
|
+
elif curr_comp.pdf_same_style_characters:
|
|
373
|
+
curr_comp.pdf_same_style_characters.pdf_character.append(space_char)
|
|
374
|
+
elif curr_comp.pdf_formula:
|
|
375
|
+
curr_comp.pdf_formula.pdf_character.append(space_char)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _get_first_char_from_composition(
|
|
379
|
+
comp: PdfParagraphComposition,
|
|
380
|
+
) -> PdfCharacter | None:
|
|
381
|
+
"""获取组成部分的第一个字符"""
|
|
382
|
+
if comp.pdf_line and comp.pdf_line.pdf_character:
|
|
383
|
+
return comp.pdf_line.pdf_character[0]
|
|
384
|
+
elif (
|
|
385
|
+
comp.pdf_same_style_characters and comp.pdf_same_style_characters.pdf_character
|
|
386
|
+
):
|
|
387
|
+
return comp.pdf_same_style_characters.pdf_character[0]
|
|
388
|
+
elif comp.pdf_formula and comp.pdf_formula.pdf_character:
|
|
389
|
+
return comp.pdf_formula.pdf_character[0]
|
|
390
|
+
elif comp.pdf_character:
|
|
391
|
+
return comp.pdf_character
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _get_last_char_from_composition(
|
|
396
|
+
comp: PdfParagraphComposition,
|
|
397
|
+
) -> PdfCharacter | None:
|
|
398
|
+
"""获取组成部分的最后一个字符"""
|
|
399
|
+
if comp.pdf_line and comp.pdf_line.pdf_character:
|
|
400
|
+
return comp.pdf_line.pdf_character[-1]
|
|
401
|
+
elif (
|
|
402
|
+
comp.pdf_same_style_characters and comp.pdf_same_style_characters.pdf_character
|
|
403
|
+
):
|
|
404
|
+
return comp.pdf_same_style_characters.pdf_character[-1]
|
|
405
|
+
elif comp.pdf_formula and comp.pdf_formula.pdf_character:
|
|
406
|
+
return comp.pdf_formula.pdf_character[-1]
|
|
407
|
+
elif comp.pdf_character:
|
|
408
|
+
return comp.pdf_character
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _add_space_dummy_chars_to_list(chars: list[PdfCharacter]) -> None:
|
|
413
|
+
"""
|
|
414
|
+
在字符列表中的适当位置添加表示空格的dummy字符。
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
chars: PdfCharacter对象列表
|
|
418
|
+
"""
|
|
419
|
+
if not chars:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
# 计算字符间距的中位数
|
|
423
|
+
distances = []
|
|
424
|
+
for i in range(len(chars) - 1):
|
|
425
|
+
distance = chars[i + 1].box.x - chars[i].box.x2
|
|
426
|
+
if distance > 1: # 只考虑正向距离
|
|
427
|
+
distances.append(distance)
|
|
428
|
+
|
|
429
|
+
# 去重后的距离
|
|
430
|
+
distinct_distances = sorted(set(distances))
|
|
431
|
+
|
|
432
|
+
if not distinct_distances:
|
|
433
|
+
median_distance = 1
|
|
434
|
+
elif len(distinct_distances) == 1:
|
|
435
|
+
median_distance = distinct_distances[0]
|
|
436
|
+
else:
|
|
437
|
+
median_distance = distinct_distances[1]
|
|
438
|
+
|
|
439
|
+
# 在需要的地方插入空格字符
|
|
440
|
+
i = 0
|
|
441
|
+
while i < len(chars) - 1:
|
|
442
|
+
curr_char = chars[i]
|
|
443
|
+
next_char = chars[i + 1]
|
|
444
|
+
|
|
445
|
+
distance = next_char.box.x - curr_char.box.x2
|
|
446
|
+
if distance >= median_distance or Layout.is_newline(curr_char, next_char):
|
|
447
|
+
# 创建一个dummy字符作为空格
|
|
448
|
+
space_box = Box(
|
|
449
|
+
x=curr_char.box.x2,
|
|
450
|
+
y=curr_char.box.y,
|
|
451
|
+
x2=curr_char.box.x2 + min(distance, median_distance),
|
|
452
|
+
y2=curr_char.box.y2,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
space_char = PdfCharacter(
|
|
456
|
+
pdf_style=curr_char.pdf_style,
|
|
457
|
+
box=space_box,
|
|
458
|
+
char_unicode=" ",
|
|
459
|
+
scale=curr_char.scale,
|
|
460
|
+
advance=space_box.x2 - space_box.x,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# 在当前位置后插入空格字符
|
|
464
|
+
chars.insert(i + 1, space_char)
|
|
465
|
+
i += 2 # 跳过刚插入的空格
|
|
466
|
+
else:
|
|
467
|
+
i += 1
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from babeldoc.document_il import il_version_1
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_pdf_style(r, g, b, font_id="china-ss", font_size=6):
|
|
5
|
+
"""
|
|
6
|
+
Create a PdfStyle object from RGB values.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
r: Red component in range 0-255
|
|
10
|
+
g: Green component in range 0-255
|
|
11
|
+
b: Blue component in range 0-255
|
|
12
|
+
font_id: Font identifier
|
|
13
|
+
font_size: Font size
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
PdfStyle object with the specified color
|
|
17
|
+
"""
|
|
18
|
+
r, g, b = [x / 255.0 for x in (r, g, b)]
|
|
19
|
+
return il_version_1.PdfStyle(
|
|
20
|
+
font_id=font_id,
|
|
21
|
+
font_size=font_size,
|
|
22
|
+
graphic_state=il_version_1.GraphicState(
|
|
23
|
+
passthrough_per_char_instruction=f"{r:.10f} {g:.10f} {b:.10f} rg",
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Generate all color styles
|
|
29
|
+
RED = il_version_1.GraphicState(
|
|
30
|
+
passthrough_per_char_instruction="1.0000000000 0.2313725490 0.1882352941 rg "
|
|
31
|
+
"1.0000000000 0.2313725490 0.1882352941 RG",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
ORANGE = il_version_1.GraphicState(
|
|
35
|
+
passthrough_per_char_instruction="1.0000000000 0.5843137255 0.0000000000 rg "
|
|
36
|
+
"1.0000000000 0.5843137255 0.0000000000 RG",
|
|
37
|
+
)
|
|
38
|
+
YELLOW = il_version_1.GraphicState(
|
|
39
|
+
passthrough_per_char_instruction="1.0000000000 0.8000000000 0.0000000000 rg "
|
|
40
|
+
"1.0000000000 0.8000000000 0.0000000000 RG",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
GREEN = il_version_1.GraphicState(
|
|
44
|
+
passthrough_per_char_instruction="0.2039215686 0.7803921569 0.3490196078 rg "
|
|
45
|
+
"0.2039215686 0.7803921569 0.3490196078 RG",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
MINT = il_version_1.GraphicState(
|
|
49
|
+
passthrough_per_char_instruction="0.0000000000 0.7803921569 0.7450980392 rg "
|
|
50
|
+
"0.0000000000 0.7803921569 0.7450980392 RG",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
TEAL = il_version_1.GraphicState(
|
|
54
|
+
passthrough_per_char_instruction="0.1882352941 0.6901960784 0.7803921569 rg "
|
|
55
|
+
"0.1882352941 0.6901960784 0.7803921569 RG",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
CYAN = il_version_1.GraphicState(
|
|
59
|
+
passthrough_per_char_instruction="0.1960784314 0.6784313725 0.9019607843 rg "
|
|
60
|
+
"0.1960784314 0.6784313725 0.9019607843 RG",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
BLUE = il_version_1.GraphicState(
|
|
64
|
+
passthrough_per_char_instruction="0.0000000000 0.4784313725 1.0000000000 rg "
|
|
65
|
+
"0.0000000000 0.4784313725 1.0000000000 RG",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
INDIGO = il_version_1.GraphicState(
|
|
69
|
+
passthrough_per_char_instruction="0.3450980392 0.3372549020 0.8392156863 rg "
|
|
70
|
+
"0.3450980392 0.3372549020 0.8392156863 RG",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
PURPLE = il_version_1.GraphicState(
|
|
74
|
+
passthrough_per_char_instruction="0.6862745098 0.3215686275 0.8705882353 rg "
|
|
75
|
+
"0.6862745098 0.3215686275 0.8705882353 RG",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
PINK = il_version_1.GraphicState(
|
|
79
|
+
passthrough_per_char_instruction="1.0000000000 0.1764705882 0.3333333333 rg "
|
|
80
|
+
"1.0000000000 0.1764705882 0.3333333333 RG",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
BROWN = il_version_1.GraphicState(
|
|
84
|
+
passthrough_per_char_instruction="0.6352941176 0.5176470588 0.3686274510 rg "
|
|
85
|
+
"0.6352941176 0.5176470588 0.3686274510 RG",
|
|
86
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import orjson
|
|
5
|
+
from xsdata.formats.dataclass.context import XmlContext
|
|
6
|
+
from xsdata.formats.dataclass.parsers import XmlParser
|
|
7
|
+
from xsdata.formats.dataclass.serializers import XmlSerializer
|
|
8
|
+
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
|
9
|
+
|
|
10
|
+
from babeldoc.document_il import il_version_1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class XMLConverter:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.parser = XmlParser()
|
|
16
|
+
config = SerializerConfig(indent=" ")
|
|
17
|
+
context = XmlContext()
|
|
18
|
+
self.serializer = XmlSerializer(context=context, config=config)
|
|
19
|
+
|
|
20
|
+
def write_xml(self, document: il_version_1.Document, path: str):
|
|
21
|
+
with Path(path).open("w", encoding="utf-8") as f:
|
|
22
|
+
f.write(self.to_xml(document))
|
|
23
|
+
|
|
24
|
+
def read_xml(self, path: str) -> il_version_1.Document:
|
|
25
|
+
with Path(path).open(encoding="utf-8") as f:
|
|
26
|
+
return self.from_xml(f.read())
|
|
27
|
+
|
|
28
|
+
def to_xml(self, document: il_version_1.Document) -> str:
|
|
29
|
+
return self.serializer.render(document)
|
|
30
|
+
|
|
31
|
+
def from_xml(self, xml: str) -> il_version_1.Document:
|
|
32
|
+
return self.parser.from_string(
|
|
33
|
+
xml,
|
|
34
|
+
il_version_1.Document,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def deepcopy(self, document: il_version_1.Document) -> il_version_1.Document:
|
|
38
|
+
return copy.deepcopy(document)
|
|
39
|
+
# return self.from_xml(self.to_xml(document))
|
|
40
|
+
|
|
41
|
+
def to_json(self, document: il_version_1.Document) -> str:
|
|
42
|
+
return orjson.dumps(
|
|
43
|
+
document,
|
|
44
|
+
option=orjson.OPT_APPEND_NEWLINE
|
|
45
|
+
| orjson.OPT_INDENT_2
|
|
46
|
+
| orjson.OPT_SORT_KEYS,
|
|
47
|
+
).decode()
|
|
48
|
+
|
|
49
|
+
def write_json(self, document: il_version_1.Document, path: str):
|
|
50
|
+
with Path(path).open("w", encoding="utf-8") as f:
|
|
51
|
+
f.write(self.to_json(document))
|
|
File without changes
|
|
File without changes
|