feat(format): B3.10 — preserve code-block / callout layout (drawing-covered blocks)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m10s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m10s
User reported that on the page 3 of the test PDF ('2. Installation
and Setup'), the curl code block was visually broken: the 5 code
lines (curl, -H, -F, -F, -F) were split, with the first 2 lines
above the gray background box and the last 3 inside (or vice versa).
Root cause: each code line is its own PDF block. The merge logic
correctly combined them into a single block (same x0, similar font,
small gap). The smart-fit then wrote the entire 5-line text into
the merged block's bbox, shrinking the font to fit. The result
no longer aligned with the fixed-extent gray background drawing.
Fix: detect when a block is covered by a colored background drawing
(code block, callout box, info box, etc.) and:
1. Mark each line as _no_merge=True so the merge logic keeps them
as separate per-line blocks
2. Each line keeps its original y position
3. The smart-fit writes each line at its own bbox, preserving
alignment with the surrounding drawing
Detection: a block is 'covered by drawing' if >= 50% of its bbox
area intersects a filled drawing on the page. This is conservative
enough to avoid false positives from drawings that merely touch a
corner of the block.
The same logic applies to callout boxes, info boxes, and any other
visual element where the background defines a fixed extent that the
text must align with. The detection is generic — no hardcoded
patterns, no font-based heuristics.
3 new tests added:
- test_code_block_lines_marked_no_merge: 5 lines inside a
background drawing all marked _no_merge=True
- test_paragraph_not_marked_no_merge: 3 plain lines (no drawing)
still merge into 1 block (regression check)
- test_code_block_end_to_end_preserves_lines: full translate
pipeline, each line stays at its own y, all inside the drawing
Total: 460 tests pass (was 457), zero regression.
This commit is contained in:
@@ -463,11 +463,21 @@ class PDFTranslator:
|
||||
a table row instead of collapsing into a single left-aligned
|
||||
column.
|
||||
|
||||
Track B3.10: when a block is COVERED by a drawing (code block
|
||||
with a background rect, callout box, etc.), split it into
|
||||
per-line blocks and mark them as ``no_merge``. Without this,
|
||||
the merge logic collapses 5 code lines into one block, and the
|
||||
smart-fit rewrites them all at the block's bbox, which
|
||||
destroys the line-by-line layout and the surrounding
|
||||
background drawing's apparent extent.
|
||||
|
||||
The detection is: if a block has >= 2 lines whose y0 is within
|
||||
``SAME_ROW_Y_TOLERANCE`` of each other, treat it as a
|
||||
multi-column row and emit one sub-block per line. Blocks whose
|
||||
lines are vertically stacked (multi-line paragraphs) keep the
|
||||
old behavior.
|
||||
old behavior, UNLESS they are covered by a drawing — in which
|
||||
case we split per-line and set ``no_merge`` to preserve the
|
||||
code-block / callout layout.
|
||||
"""
|
||||
import fitz
|
||||
|
||||
@@ -476,6 +486,44 @@ class PDFTranslator:
|
||||
|
||||
SAME_ROW_Y_TOLERANCE = 3.0 # points
|
||||
|
||||
# B3.10: pre-compute the set of page drawings with a fill
|
||||
# (colored rects, callout boxes, etc.) so we can detect
|
||||
# "code block" or "callout" patterns and avoid merging their
|
||||
# constituent lines.
|
||||
drawing_rects: list = []
|
||||
if hasattr(page, "get_drawings"):
|
||||
try:
|
||||
for d in page.get_drawings():
|
||||
r = d.get("rect")
|
||||
if r is not None and d.get("fill") is not None:
|
||||
drawing_rects.append(fitz.Rect(r))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _block_covered_by_drawing(block_bbox_tuple) -> bool:
|
||||
"""True if the block's bbox is covered by a filled drawing.
|
||||
|
||||
We use "covered" loosely — a substantial intersection
|
||||
(>= 50% of the block's area) qualifies. This avoids
|
||||
false positives from drawings that merely touch a corner
|
||||
of the block.
|
||||
"""
|
||||
if not drawing_rects:
|
||||
return False
|
||||
b = fitz.Rect(block_bbox_tuple)
|
||||
block_area = b.width * b.height
|
||||
if block_area <= 0:
|
||||
return False
|
||||
for dr in drawing_rects:
|
||||
if not b.intersects(dr):
|
||||
continue
|
||||
# Compute intersection area
|
||||
inter = b & dr
|
||||
inter_area = inter.width * inter.height
|
||||
if inter_area / block_area >= 0.5:
|
||||
return True
|
||||
return False
|
||||
|
||||
for block in data.get("blocks", []):
|
||||
if block.get("type") != 0:
|
||||
continue
|
||||
@@ -500,8 +548,18 @@ class PDFTranslator:
|
||||
)
|
||||
)
|
||||
|
||||
if has_horizontal_layout:
|
||||
# Multi-column row: emit one block per line.
|
||||
# B3.10: detect code-block / callout pattern — a block
|
||||
# whose lines are covered by a colored background drawing.
|
||||
# Note: each code line is its own PDF block, so we check
|
||||
# the block bbox (which is the line's bbox) against the
|
||||
# drawing, not "len(lines) > 1".
|
||||
covered_by_drawing = (
|
||||
not has_horizontal_layout
|
||||
and _block_covered_by_drawing(block["bbox"])
|
||||
)
|
||||
|
||||
if has_horizontal_layout or covered_by_drawing:
|
||||
# Multi-column row OR code block: emit one block per line.
|
||||
for line in lines:
|
||||
span_parts = []
|
||||
spans_info = []
|
||||
@@ -538,7 +596,8 @@ class PDFTranslator:
|
||||
"line_count": 1,
|
||||
"translated": None,
|
||||
"sub_bboxes": [tuple(line["bbox"])],
|
||||
"_is_table_cell": True,
|
||||
"_is_table_cell": has_horizontal_layout,
|
||||
"_no_merge": covered_by_drawing,
|
||||
})
|
||||
continue
|
||||
|
||||
@@ -632,6 +691,15 @@ class PDFTranslator:
|
||||
a_bbox = a["bbox"]
|
||||
b_bbox = b["bbox"]
|
||||
|
||||
# Track B3.10: never merge blocks marked as no_merge.
|
||||
# These are lines inside a code block, callout box, or any
|
||||
# other structure that needs to preserve its line-by-line
|
||||
# layout (e.g. each line of a curl command must stay at its
|
||||
# own y position to remain aligned with the background
|
||||
# drawing that covers them).
|
||||
if a.get("_no_merge") or b.get("_no_merge"):
|
||||
return False
|
||||
|
||||
# Must have similar font size (within 20%)
|
||||
if abs(a["font_size"] - b["font_size"]) > max(a["font_size"], b["font_size"]) * 0.2:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user