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