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,180 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import cv2
5
+ import numpy as np
6
+ from pymupdf import Document
7
+
8
+ from babeldoc.document_il import il_version_1
9
+ from babeldoc.document_il.utils.style_helper import GREEN
10
+ from babeldoc.translation_config import TranslationConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class LayoutParser:
16
+ stage_name = "解析页面布局"
17
+
18
+ def __init__(self, translation_config: TranslationConfig):
19
+ self.translation_config = translation_config
20
+ self.model = translation_config.doc_layout_model
21
+
22
+ def _save_debug_image(self, image: np.ndarray, layout, page_number: int):
23
+ """Save debug image with drawn boxes if debug mode is enabled."""
24
+ if not self.translation_config.debug:
25
+ return
26
+
27
+ debug_dir = Path(self.translation_config.get_working_file_path("ocr-box-image"))
28
+ debug_dir.mkdir(parents=True, exist_ok=True)
29
+
30
+ # Draw boxes on the image
31
+ debug_image = image.copy()
32
+ for box in layout.boxes:
33
+ x0, y0, x1, y1 = box.xyxy
34
+ cv2.rectangle(
35
+ debug_image,
36
+ (int(x0), int(y0)),
37
+ (int(x1), int(y1)),
38
+ (0, 255, 0),
39
+ 2,
40
+ )
41
+ # Add text label
42
+ cv2.putText(
43
+ debug_image,
44
+ layout.names[box.cls],
45
+ (int(x0), int(y0) - 5),
46
+ cv2.FONT_HERSHEY_SIMPLEX,
47
+ 0.5,
48
+ (0, 255, 0),
49
+ 1,
50
+ )
51
+
52
+ # Save the image
53
+ output_path = debug_dir / f"{page_number}.jpg"
54
+ cv2.imwrite(str(output_path), debug_image)
55
+
56
+ def _save_debug_box_to_page(self, page: il_version_1.Page):
57
+ """Save debug boxes and text labels to the PDF page."""
58
+ if not self.translation_config.debug:
59
+ return
60
+
61
+ color = GREEN
62
+
63
+ for layout in page.page_layout:
64
+ # Create a rectangle box
65
+ rect = il_version_1.PdfRectangle(
66
+ box=il_version_1.Box(
67
+ x=layout.box.x,
68
+ y=layout.box.y,
69
+ x2=layout.box.x2,
70
+ y2=layout.box.y2,
71
+ ),
72
+ graphic_state=color,
73
+ debug_info=True,
74
+ )
75
+ page.pdf_rectangle.append(rect)
76
+
77
+ # Create text label at top-left corner
78
+ # Note: PDF coordinates are from bottom-left,
79
+ # so we use y2 for top position
80
+ style = il_version_1.PdfStyle(
81
+ font_id="china-ss",
82
+ font_size=4,
83
+ graphic_state=color,
84
+ )
85
+ page.pdf_paragraph.append(
86
+ il_version_1.PdfParagraph(
87
+ first_line_indent=False,
88
+ box=il_version_1.Box(
89
+ x=layout.box.x,
90
+ y=layout.box.y2,
91
+ x2=layout.box.x2,
92
+ y2=layout.box.y2 + 5,
93
+ ),
94
+ vertical=False,
95
+ pdf_style=style,
96
+ unicode=layout.class_name,
97
+ pdf_paragraph_composition=[
98
+ il_version_1.PdfParagraphComposition(
99
+ pdf_same_style_unicode_characters=il_version_1.PdfSameStyleUnicodeCharacters(
100
+ unicode=layout.class_name,
101
+ pdf_style=style,
102
+ debug_info=True,
103
+ ),
104
+ ),
105
+ ],
106
+ xobj_id=-1,
107
+ ),
108
+ )
109
+
110
+ def process(self, docs: il_version_1.Document, mupdf_doc: Document):
111
+ """Generate layouts for all pages that need to be translated."""
112
+ # Get pages that need to be translated
113
+ pages_to_translate = [
114
+ page
115
+ for page in docs.page
116
+ if self.translation_config.should_translate_page(page.page_number + 1)
117
+ ]
118
+ total = len(pages_to_translate)
119
+ with self.translation_config.progress_monitor.stage_start(
120
+ self.stage_name,
121
+ total,
122
+ ) as progress:
123
+ # Process pages in batches
124
+ batch_size = 16
125
+ for i in range(0, total, batch_size):
126
+ self.translation_config.raise_if_cancelled()
127
+ batch_pages = pages_to_translate[i : i + batch_size]
128
+
129
+ # Prepare batch images
130
+ batch_images = []
131
+ for page in batch_pages:
132
+ pix = mupdf_doc[page.page_number].get_pixmap(dpi=72)
133
+ image = np.fromstring(pix.samples, np.uint8).reshape(
134
+ pix.height,
135
+ pix.width,
136
+ 3,
137
+ )[:, :, ::-1]
138
+ batch_images.append(image)
139
+
140
+ # Get predictions for the batch
141
+ layouts_batch = self.model.predict(batch_images, batch_size=batch_size)
142
+
143
+ # Process predictions for each page
144
+ for page, layouts in zip(batch_pages, layouts_batch, strict=False):
145
+ page_layouts = []
146
+ self._save_debug_image(
147
+ batch_images[batch_pages.index(page)],
148
+ layouts,
149
+ page.page_number + 1,
150
+ )
151
+ for layout in layouts.boxes:
152
+ # Convert coordinate system from picture to il
153
+ # system to the il coordinate system
154
+ x0, y0, x1, y1 = layout.xyxy
155
+ pix = mupdf_doc[page.page_number].get_pixmap()
156
+ h, w = pix.height, pix.width
157
+ x0, y0, x1, y1 = (
158
+ np.clip(int(x0 - 1), 0, w - 1),
159
+ np.clip(int(h - y1 - 1), 0, h - 1),
160
+ np.clip(int(x1 + 1), 0, w - 1),
161
+ np.clip(int(h - y0 + 1), 0, h - 1),
162
+ )
163
+ page_layout = il_version_1.PageLayout(
164
+ id=len(page_layouts) + 1,
165
+ box=il_version_1.Box(
166
+ x0.item(),
167
+ y0.item(),
168
+ x1.item(),
169
+ y1.item(),
170
+ ),
171
+ conf=layout.conf.item(),
172
+ class_name=layouts.names[layout.cls],
173
+ )
174
+ page_layouts.append(page_layout)
175
+
176
+ page.page_layout = page_layouts
177
+ self._save_debug_box_to_page(page)
178
+ progress.advance(1)
179
+
180
+ return docs
@@ -0,0 +1,452 @@
1
+ import logging
2
+ import random
3
+ import re
4
+ from typing import Literal
5
+
6
+ from babeldoc.document_il import Box
7
+ from babeldoc.document_il import Page
8
+ from babeldoc.document_il import PdfCharacter
9
+ from babeldoc.document_il import PdfLine
10
+ from babeldoc.document_il import PdfParagraph
11
+ from babeldoc.document_il import PdfParagraphComposition
12
+ from babeldoc.document_il.utils.layout_helper import Layout
13
+ from babeldoc.document_il.utils.layout_helper import add_space_dummy_chars
14
+ from babeldoc.document_il.utils.layout_helper import get_char_unicode_string
15
+ from babeldoc.translation_config import TranslationConfig
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Base58 alphabet (Bitcoin style, without numbers 0, O, I, l)
20
+ BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
21
+
22
+
23
+ def generate_base58_id(length: int = 5) -> str:
24
+ """Generate a random base58 ID of specified length."""
25
+ return "".join(random.choice(BASE58_ALPHABET) for _ in range(length))
26
+
27
+
28
+ class ParagraphFinder:
29
+ stage_name = "解析段落"
30
+
31
+ def __init__(self, translation_config: TranslationConfig):
32
+ self.translation_config = translation_config
33
+
34
+ def update_paragraph_data(self, paragraph: PdfParagraph, update_unicode=False):
35
+ if not paragraph.pdf_paragraph_composition:
36
+ return
37
+
38
+ chars = []
39
+ for composition in paragraph.pdf_paragraph_composition:
40
+ if composition.pdf_line:
41
+ chars.extend(composition.pdf_line.pdf_character)
42
+ elif composition.pdf_formula:
43
+ chars.extend(composition.pdf_formula.pdf_character)
44
+ elif composition.pdf_same_style_unicode_characters:
45
+ continue
46
+ else:
47
+ logger.error(
48
+ "Unexpected composition type"
49
+ " in PdfParagraphComposition. "
50
+ "This type only appears in the IL "
51
+ "after the translation is completed.",
52
+ )
53
+ continue
54
+
55
+ if update_unicode and chars:
56
+ paragraph.unicode = get_char_unicode_string(chars)
57
+ if not chars:
58
+ return
59
+ # 更新边界框
60
+ min_x = min(char.box.x for char in chars)
61
+ min_y = min(char.box.y for char in chars)
62
+ max_x = max(char.box.x2 for char in chars)
63
+ max_y = max(char.box.y2 for char in chars)
64
+ paragraph.box = Box(min_x, min_y, max_x, max_y)
65
+ paragraph.vertical = chars[0].vertical
66
+ paragraph.xobj_id = chars[0].xobj_id
67
+
68
+ paragraph.first_line_indent = False
69
+ if (
70
+ paragraph.pdf_paragraph_composition[0].pdf_line
71
+ and paragraph.pdf_paragraph_composition[0].pdf_line.pdf_character[0].box.x
72
+ - paragraph.box.x
73
+ > 1
74
+ ):
75
+ paragraph.first_line_indent = True
76
+
77
+ def update_line_data(self, line: PdfLine):
78
+ min_x = min(char.box.x for char in line.pdf_character)
79
+ min_y = min(char.box.y for char in line.pdf_character)
80
+ max_x = max(char.box.x2 for char in line.pdf_character)
81
+ max_y = max(char.box.y2 for char in line.pdf_character)
82
+ line.box = Box(min_x, min_y, max_x, max_y)
83
+
84
+ def process(self, document):
85
+ with self.translation_config.progress_monitor.stage_start(
86
+ self.stage_name,
87
+ len(document.page),
88
+ ) as pbar:
89
+ for page in document.page:
90
+ self.translation_config.raise_if_cancelled()
91
+ self.process_page(page)
92
+ pbar.advance()
93
+
94
+ def bbox_overlap(self, bbox1: Box, bbox2: Box) -> bool:
95
+ return (
96
+ bbox1.x < bbox2.x2
97
+ and bbox1.x2 > bbox2.x
98
+ and bbox1.y < bbox2.y2
99
+ and bbox1.y2 > bbox2.y
100
+ )
101
+
102
+ def process_page(self, page: Page):
103
+ # 第一步:根据 layout 创建 paragraphs
104
+ # 在这一步中,page.pdf_character 中的字符会被移除
105
+ paragraphs = self.create_paragraphs(page)
106
+ page.pdf_paragraph = paragraphs
107
+
108
+ # 第二步:处理段落中的空格和换行符
109
+ for paragraph in paragraphs:
110
+ add_space_dummy_chars(paragraph)
111
+ self.process_paragraph_spacing(paragraph)
112
+ self.update_paragraph_data(paragraph)
113
+
114
+ # 第三步:计算所有行宽度的中位数
115
+ median_width = self.calculate_median_line_width(paragraphs)
116
+
117
+ # 第四步:处理独立段落
118
+ self.process_independent_paragraphs(paragraphs, median_width)
119
+
120
+ for paragraph in paragraphs:
121
+ self.update_paragraph_data(paragraph, update_unicode=True)
122
+
123
+ def is_isolated_formula(self, char: PdfCharacter):
124
+ return char.char_unicode in (
125
+ "(cid:122)",
126
+ "(cid:123)",
127
+ "(cid:124)",
128
+ "(cid:125)",
129
+ )
130
+
131
+ def create_paragraphs(self, page: Page) -> list[PdfParagraph]:
132
+ paragraphs: list[PdfParagraph] = []
133
+ if page.pdf_paragraph:
134
+ paragraphs.extend(page.pdf_paragraph)
135
+ page.pdf_paragraph = []
136
+
137
+ current_paragraph: PdfParagraph | None = None
138
+ current_layout: Layout | None = None
139
+ current_line_chars: list[PdfCharacter] = []
140
+ skip_chars = []
141
+
142
+ for char in page.pdf_character:
143
+ char_layout = self.get_layout(char, page)
144
+ if not self.is_text_layout(char_layout) or self.is_isolated_formula(char):
145
+ skip_chars.append(char)
146
+ continue
147
+
148
+ # 检查是否需要开始新行
149
+ if current_line_chars and Layout.is_newline(current_line_chars[-1], char):
150
+ # 创建新行
151
+ if current_line_chars:
152
+ line = self.create_line(current_line_chars)
153
+ if current_paragraph is None:
154
+ current_paragraph = PdfParagraph(
155
+ pdf_paragraph_composition=[line],
156
+ debug_id=generate_base58_id(),
157
+ )
158
+ paragraphs.append(current_paragraph)
159
+ else:
160
+ current_paragraph.pdf_paragraph_composition.append(line)
161
+ self.update_paragraph_data(current_paragraph)
162
+ current_line_chars = []
163
+
164
+ # 检查是否需要开始新段落
165
+ if (
166
+ current_layout is None
167
+ or char_layout.id != current_layout.id
168
+ or ( # 不是同一个 xobject
169
+ current_line_chars
170
+ and current_line_chars[-1].xobj_id != char.xobj_id
171
+ )
172
+ ):
173
+ if current_line_chars:
174
+ line = self.create_line(current_line_chars)
175
+ if current_paragraph is not None:
176
+ current_paragraph.pdf_paragraph_composition.append(line)
177
+ self.update_paragraph_data(current_paragraph)
178
+ else:
179
+ current_paragraph = PdfParagraph(
180
+ pdf_paragraph_composition=[line],
181
+ debug_id=generate_base58_id(),
182
+ )
183
+ self.update_paragraph_data(current_paragraph)
184
+ paragraphs.append(current_paragraph)
185
+ current_line_chars = []
186
+ current_paragraph = None
187
+ current_layout = char_layout
188
+
189
+ current_line_chars.append(char)
190
+
191
+ # 处理最后一行的字符
192
+ if current_line_chars:
193
+ line = self.create_line(current_line_chars)
194
+ if current_paragraph is None:
195
+ current_paragraph = PdfParagraph(
196
+ pdf_paragraph_composition=[line],
197
+ debug_id=generate_base58_id(),
198
+ )
199
+ paragraphs.append(current_paragraph)
200
+ else:
201
+ current_paragraph.pdf_paragraph_composition.append(line)
202
+ self.update_paragraph_data(current_paragraph)
203
+
204
+ page.pdf_character = skip_chars
205
+
206
+ return paragraphs
207
+
208
+ def process_paragraph_spacing(self, paragraph: PdfParagraph):
209
+ if not paragraph.pdf_paragraph_composition:
210
+ return
211
+
212
+ # 处理行级别的空格
213
+ processed_lines = []
214
+ for composition in paragraph.pdf_paragraph_composition:
215
+ if not composition.pdf_line:
216
+ processed_lines.append(composition)
217
+ continue
218
+
219
+ line = composition.pdf_line
220
+ if not "".join(
221
+ x.char_unicode for x in line.pdf_character
222
+ ).strip(): # 跳过完全空白的行
223
+ continue
224
+
225
+ # 处理行内字符的尾随空格
226
+ processed_chars = []
227
+ for char in line.pdf_character:
228
+ if not char.char_unicode.isspace():
229
+ processed_chars = processed_chars + [char]
230
+ elif processed_chars: # 只有在有非空格字符后才考虑保留空格
231
+ processed_chars.append(char)
232
+
233
+ # 移除尾随空格
234
+ while processed_chars and processed_chars[-1].char_unicode.isspace():
235
+ processed_chars.pop()
236
+
237
+ if processed_chars: # 如果行内还有字符
238
+ line = self.create_line(processed_chars)
239
+ processed_lines.append(line)
240
+
241
+ paragraph.pdf_paragraph_composition = processed_lines
242
+ self.update_paragraph_data(paragraph)
243
+
244
+ def is_text_layout(self, layout: Layout):
245
+ return layout is not None and layout.name in [
246
+ "plain text",
247
+ "title",
248
+ "abandon",
249
+ "figure_caption",
250
+ "table_caption",
251
+ ]
252
+
253
+ def get_layout(
254
+ self,
255
+ char: PdfCharacter,
256
+ page: Page,
257
+ xy_mode: Literal["topleft"]
258
+ | Literal["bottomright"]
259
+ | Literal["middle"] = "middle",
260
+ ):
261
+ tl, br, md = [
262
+ self._get_layout(char, page, mode)
263
+ for mode in ["topleft", "bottomright", "middle"]
264
+ ]
265
+ if tl is not None and tl.name == "isolate_formula":
266
+ return tl
267
+ if br is not None and br.name == "isolate_formula":
268
+ return br
269
+ if md is not None and md.name == "isolate_formula":
270
+ return md
271
+
272
+ if md is not None:
273
+ return md
274
+ if tl is not None:
275
+ return tl
276
+ return br
277
+
278
+ def _get_layout(
279
+ self,
280
+ char: PdfCharacter,
281
+ page: Page,
282
+ xy_mode: Literal["topleft"]
283
+ | Literal["bottomright"]
284
+ | Literal["middle"] = "middle",
285
+ ):
286
+ # 这几个符号,解析出来的大小经常只有实际大小的一点点。
287
+ # if (
288
+ # xy_mode != "bottomright"
289
+ # and char.char_unicode in HEIGHT_NOT_USFUL_CHAR_IN_CHAR
290
+ # ):
291
+ # return self.get_layout(char, page, "bottomright")
292
+ # current layouts
293
+ # {
294
+ # "title",
295
+ # "plain text",
296
+ # "abandon",
297
+ # "figure",
298
+ # "figure_caption",
299
+ # "table",
300
+ # "table_caption",
301
+ # "table_footnote",
302
+ # "isolate_formula",
303
+ # "formula_caption",
304
+ # }
305
+ layout_priority = [
306
+ "formula_caption",
307
+ "isolate_formula",
308
+ "table_footnote",
309
+ "table",
310
+ "figure",
311
+ "table_caption",
312
+ "figure_caption",
313
+ "abandon",
314
+ "plain text",
315
+ "title",
316
+ ]
317
+ char_box = char.box
318
+ if xy_mode == "topleft":
319
+ char_x = char_box.x
320
+ char_y = char_box.y2
321
+ elif xy_mode == "bottomright":
322
+ char_x = char_box.x2
323
+ char_y = char_box.y
324
+ elif xy_mode == "middle":
325
+ char_x = (char_box.x + char_box.x2) / 2
326
+ char_y = (char_box.y + char_box.y2) / 2
327
+ else:
328
+ logger.error(f"Invalid xy_mode: {xy_mode}")
329
+ return self.get_layout(char, page, "middle")
330
+ # 按照优先级顺序检查每种布局
331
+ matching_layouts = {}
332
+ for layout in page.page_layout:
333
+ layout_box = layout.box
334
+ if (
335
+ layout_box.x <= char_x <= layout_box.x2
336
+ and layout_box.y <= char_y <= layout_box.y2
337
+ ):
338
+ matching_layouts[layout.class_name] = Layout(
339
+ layout.id,
340
+ layout.class_name,
341
+ )
342
+
343
+ # 按照优先级返回最高优先级的布局
344
+ for layout_name in layout_priority:
345
+ if layout_name in matching_layouts:
346
+ return matching_layouts[layout_name]
347
+
348
+ return None
349
+
350
+ def create_line(self, chars: list[PdfCharacter]) -> PdfParagraphComposition:
351
+ assert chars
352
+
353
+ line = PdfLine(pdf_character=chars)
354
+ self.update_line_data(line)
355
+ return PdfParagraphComposition(pdf_line=line)
356
+
357
+ def calculate_median_line_width(self, paragraphs: list[PdfParagraph]) -> float:
358
+ # 收集所有行的宽度
359
+ line_widths = []
360
+ for paragraph in paragraphs:
361
+ for composition in paragraph.pdf_paragraph_composition:
362
+ if composition.pdf_line:
363
+ line = composition.pdf_line
364
+ line_widths.append(line.box.x2 - line.box.x)
365
+
366
+ if not line_widths:
367
+ return 0.0
368
+
369
+ # 计算中位数
370
+ line_widths.sort()
371
+ mid = len(line_widths) // 2
372
+ if len(line_widths) % 2 == 0:
373
+ return (line_widths[mid - 1] + line_widths[mid]) / 2
374
+ return line_widths[mid]
375
+
376
+ def process_independent_paragraphs(
377
+ self,
378
+ paragraphs: list[PdfParagraph],
379
+ median_width: float,
380
+ ):
381
+ i = 0
382
+ while i < len(paragraphs):
383
+ paragraph = paragraphs[i]
384
+ if len(paragraph.pdf_paragraph_composition) <= 1: # 跳过只有一行的段落
385
+ i += 1
386
+ continue
387
+
388
+ j = 1
389
+ while j < len(paragraph.pdf_paragraph_composition):
390
+ prev_composition = paragraph.pdf_paragraph_composition[j - 1]
391
+ if not prev_composition.pdf_line:
392
+ j += 1
393
+ continue
394
+
395
+ prev_line = prev_composition.pdf_line
396
+ prev_width = prev_line.box.x2 - prev_line.box.x
397
+ prev_text = "".join([c.char_unicode for c in prev_line.pdf_character])
398
+
399
+ # 检查是否包含连续的点(至少 20 个)
400
+ # 如果有至少连续 20 个点,则代表这是目录条目
401
+ if re.search(r"\.{20,}", prev_text):
402
+ # 创建新的段落
403
+ new_paragraph = PdfParagraph(
404
+ box=Box(0, 0, 0, 0), # 临时边界框
405
+ pdf_paragraph_composition=(
406
+ paragraph.pdf_paragraph_composition[j:]
407
+ ),
408
+ unicode="",
409
+ debug_id=generate_base58_id(),
410
+ )
411
+ # 更新原段落
412
+ paragraph.pdf_paragraph_composition = (
413
+ paragraph.pdf_paragraph_composition[:j]
414
+ )
415
+
416
+ # 更新两个段落的数据
417
+ self.update_paragraph_data(paragraph)
418
+ self.update_paragraph_data(new_paragraph)
419
+
420
+ # 在原段落后插入新段落
421
+ paragraphs.insert(i + 1, new_paragraph)
422
+ break
423
+
424
+ # 如果前一行宽度小于中位数的一半,将当前行及后续行分割成新段落
425
+ if (
426
+ self.translation_config.split_short_lines
427
+ and prev_width
428
+ < median_width * self.translation_config.short_line_split_factor
429
+ ):
430
+ # 创建新的段落
431
+ new_paragraph = PdfParagraph(
432
+ box=Box(0, 0, 0, 0), # 临时边界框
433
+ pdf_paragraph_composition=(
434
+ paragraph.pdf_paragraph_composition[j:]
435
+ ),
436
+ unicode="",
437
+ debug_id=generate_base58_id(),
438
+ )
439
+ # 更新原段落
440
+ paragraph.pdf_paragraph_composition = (
441
+ paragraph.pdf_paragraph_composition[:j]
442
+ )
443
+
444
+ # 更新两个段落的数据
445
+ self.update_paragraph_data(paragraph)
446
+ self.update_paragraph_data(new_paragraph)
447
+
448
+ # 在原段落后插入新段落
449
+ paragraphs.insert(i + 1, new_paragraph)
450
+ break
451
+ j += 1
452
+ i += 1