diff --git a/mineru/model/flash/pdf/line_merging.py b/mineru/model/flash/pdf/line_merging.py index eaf081d61..1a36f2488 100644 --- a/mineru/model/flash/pdf/line_merging.py +++ b/mineru/model/flash/pdf/line_merging.py @@ -63,8 +63,14 @@ def union(left_index: int, right_index: int) -> None: if left_root != right_root: parents[right_root] = left_root + # 只枚举满足必要分类条件的行对,组内仍按原索引递增,保持并查集认领顺序。 + compatible_indices: dict[tuple[int, bool, str | None], list[int]] = {} + for index, line in enumerate(lines): + compatible_indices.setdefault((line.angle, line.formula_candidate_only, line.semantic_type), []).append(index) for left_index, left_line in enumerate(lines): - for right_index in range(left_index + 1, len(lines)): + for right_index in compatible_indices[(left_line.angle, left_line.formula_candidate_only, left_line.semantic_type)]: + if right_index <= left_index: + continue right_line = lines[right_index] if _can_merge_same_baseline_pair( left_line, @@ -531,8 +537,6 @@ def _can_merge_same_baseline_pair( return False if first.visual_row_id == second.visual_row_id and (first.split_from_row or second.split_from_row): return False - if _connection_crosses_table(first.bbox, second.bbox, table_bboxes): - return False first_height = _line_effective_height(first, first_bbox) second_height = _line_effective_height(second, second_bbox) has_compatible_dominant_font = not ( @@ -548,13 +552,13 @@ def _can_merge_same_baseline_pair( second_bbox, second_height, ): - return True + return not _connection_crosses_table(first.bbox, second.bbox, table_bboxes) return _touching_same_baseline_geometry( first_bbox, first_height, second_bbox, second_height, - ) + ) and not _connection_crosses_table(first.bbox, second.bbox, table_bboxes) def _touching_same_baseline_geometry( diff --git a/mineru/model/flash/pdf/native_text.py b/mineru/model/flash/pdf/native_text.py index 7a41d8c82..1ba8ab50a 100644 --- a/mineru/model/flash/pdf/native_text.py +++ b/mineru/model/flash/pdf/native_text.py @@ -811,23 +811,28 @@ def _merge_native_inline_scripts( candidates: list[tuple[float, int, int, Literal["prefix", "suffix"]]] = [] detached_candidate_pairs: set[tuple[int, int, Literal["prefix", "suffix"]]] = set() + # 候选生成期间行不会修改,按本轮索引缓存字符统计,避免每对行重算字号中位数。 + compact_texts = ["".join(char for char in line.text if not char.isspace()) for line in lines] + reference_markers = [_INLINE_REFERENCE_MARKER_RE.fullmatch(text) is not None for text in compact_texts] + local_bboxes = [_rotate_bbox_to_upright(line.bbox, page_size, line.angle) for line in lines] + canonical_scales = [_native_typographic_scale(line) for line in lines] for small_index, small in enumerate(lines): - compact_text = "".join(char for char in small.text if not char.isspace()) + compact_text = compact_texts[small_index] if not compact_text: continue - small_local_bbox = _rotate_bbox_to_upright(small.bbox, page_size, small.angle) + small_local_bbox = local_bboxes[small_index] for base_index, base in enumerate(lines): if small_index == base_index or small.angle != base.angle or small.visual_row_id == base.visual_row_id: continue if small.effective_height <= 0 or base.effective_height <= 0: continue - canonical_small_scale = _native_typographic_scale(small) - canonical_base_scale = _native_typographic_scale(base) + canonical_small_scale = canonical_scales[small_index] + canonical_base_scale = canonical_scales[base_index] legacy_small_scale = max(0.1, small.effective_height) legacy_base_scale = max(0.1, base.effective_height) legacy_ratio = legacy_small_scale / legacy_base_scale use_canonical_reference_scale = ( - _INLINE_REFERENCE_MARKER_RE.fullmatch(compact_text) is not None + reference_markers[small_index] and not 0.35 <= legacy_ratio <= 0.8 and 0.35 <= canonical_small_scale / canonical_base_scale <= 0.8 ) @@ -838,7 +843,7 @@ def _merge_native_inline_scripts( continue if len(compact_text) > 8 and small_local_bbox[2] - small_local_bbox[0] > 3.0 * base_scale: continue - base_local_bbox = _rotate_bbox_to_upright(base.bbox, page_size, base.angle) + base_local_bbox = local_bboxes[base_index] vertical_overlap = max( 0.0, min(small_local_bbox[3], base_local_bbox[3]) - max(small_local_bbox[1], base_local_bbox[1]), diff --git a/mineru/model/flash/pdf/text_styles.py b/mineru/model/flash/pdf/text_styles.py index 700cbdebb..5ff8225f6 100644 --- a/mineru/model/flash/pdf/text_styles.py +++ b/mineru/model/flash/pdf/text_styles.py @@ -1610,14 +1610,14 @@ def _script_line_char_roles( local_origins, memberships, ) - strong_structural_roles = _strong_structural_script_roles( - local_chars, - local_tight_bboxes, - local_origins, - ) if bool(getattr(line, "compact_formula_cluster", False)) or ( bool(getattr(line, "restored_inline_cluster", False)) and bool(regions) ): + strong_structural_roles = _strong_structural_script_roles( + local_chars, + local_tight_bboxes, + local_origins, + ) roles = [strong_structural_roles.get(index, "body") for index in range(len(roles))] for index, char in enumerate(chars): char_idx = char.get("char_idx") diff --git a/tests/unittest/test_flash_pdf_native_text.py b/tests/unittest/test_flash_pdf_native_text.py index 01a95be40..4b1c83235 100644 --- a/tests/unittest/test_flash_pdf_native_text.py +++ b/tests/unittest/test_flash_pdf_native_text.py @@ -48,6 +48,40 @@ def _line( } +def test_inline_script_scale_cache_is_linear_and_local_to_each_pass(monkeypatch: pytest.MonkeyPatch) -> None: + """每轮字号计算随行数线性增长,后续轮次重新读取已变化的字符字号。""" + + lines = [ + models._LineItem( + text="body", + bbox=(10.0, index * 30.0, 30.0, index * 30.0 + 10.0), + angle=0, + source_index=index, + visual_row_id=index, + effective_height=10.0, + chars=[{"char": "a", "font": {"size": 10.0}}], + ) + for index in range(20) + ] + observed: list[float] = [] + original = native_text._native_typographic_scale + + def record_scale(line: models._LineItem) -> float: + """记录真实字号计算,验证缓存不跨越行变更边界。""" + + value = original(line) + observed.append(value) + return value + + monkeypatch.setattr(native_text, "_native_typographic_scale", record_scale) + native_text._merge_native_inline_scripts(lines, (100.0, 700.0)) + assert observed == [10.0] * 20 + lines[0].chars[0]["font"]["size"] = 12.0 + observed.clear() + native_text._merge_native_inline_scripts(lines, (100.0, 700.0)) + assert observed == [12.0, *([10.0] * 19)] + + def test_private_use_decorative_rule_becomes_axis_line() -> None: """验证页首宽幅私用区重复字形不再进入文本输出。"""