All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m32s
User asked whether B3.6+B3.7 are generic for ALL PDFs or just for the
test_pdf.pdf. Audit found 2 genericity bugs:
1. _populate_next_block_y was sorting blocks globally by y0. In a
multi-column PDF (journals, brochures, newspapers), the 'next
block' of a left-column block would point to the right-column
block at the same y, which is wrong. Fix: group blocks into
columns by x0 proximity (15pt tolerance), then sort each column
by y0. Each block's next_block_y is the y0 of its column-mate
directly below it, not just the next block in y-order globally.
2. max_expand_y could go negative if next_block_y was above the
current block (rare edge case in extracted blocks with weird
bbox ordering). A negative max_expand_y would create an invalid
fitz.Rect with y1 < y0, causing silent failures. Fix: clamp
max_expand_y to >= 0.
7 new tests added:
- test_two_columns_get_separate_next_block_y: 2-col layout,
left and right columns get independent next_block_y mappings
- test_centered_full_width_header_gets_own_column: full-width
header between 2 columns is its own column
- test_three_columns: 3-column newspaper layout
- test_single_block_page, test_empty_block_list: edge cases
- test_max_expand_y_clamped_to_zero: negative-expansion safety
- test_two_column_pdf_translation_end_to_end: e2e test on a
2-col journal PDF, 4 input blocks -> 4 output blocks preserved
at correct positions, no cross-column overlap
Visual verification:
scripts/verify_b3_8_multicolumn.py renders a 2-col journal PDF
before and after translation, confirms 4 left + 4 right blocks
preserved at exact positions.
Total tests: 453 (was 446), zero regression.
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
"""
|
|
B3.8 visual verification on a REAL multi-column layout.
|
|
|
|
Generates a 2-column journal-style PDF (3 paragraphs in each column,
|
|
each paragraph = 3 lines), then translates it with B3.8 enabled.
|
|
|
|
Without B3.8, the next_block_y of a left-column block would point to
|
|
the right-column block at the same y (the closest in y-order), which
|
|
is wrong — the smart-fit would never let left-column text expand down
|
|
into the left-column space below it.
|
|
"""
|
|
import sys
|
|
sys.path.insert(0, '.')
|
|
import fitz
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
REPO_ROOT = Path('.').resolve()
|
|
OUT_DIR = REPO_ROOT / 'scripts' / '_renders_b3_8'
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def build_2col_journal_pdf(path: Path) -> None:
|
|
"""Build a 2-column journal-style PDF."""
|
|
doc = fitz.open()
|
|
page = doc.new_page(width=612, height=792)
|
|
# Centered title
|
|
title_rect = fitz.Rect(72, 60, 540, 100)
|
|
page.insert_textbox(title_rect, "Article Title", fontsize=20)
|
|
|
|
# Author under title
|
|
page.insert_text((250, 120), "By John Doe", fontsize=10)
|
|
|
|
# Left column: 3 paragraphs
|
|
left_x0 = 72
|
|
left_x1 = 300
|
|
paras_left = [
|
|
"This is the first paragraph of the left column. It has multiple lines that span across the column width.",
|
|
"Second paragraph of the left column. This text continues to fill the left side of the page.",
|
|
"Third and final paragraph on the left side. The text wraps to fill the available space.",
|
|
]
|
|
y = 170
|
|
for p in paras_left:
|
|
rect = fitz.Rect(left_x0, y, left_x1, y + 80)
|
|
page.insert_textbox(rect, p, fontsize=10)
|
|
y += 100
|
|
|
|
# Right column: 3 paragraphs (same y positions as left, so the
|
|
# "wrong" next-block mapping would matter here)
|
|
right_x0 = 312
|
|
right_x1 = 540
|
|
paras_right = [
|
|
"Right column, first paragraph. This should stay in the right column and not be affected by left.",
|
|
"Right column, second paragraph. Independent of the left column's translation length.",
|
|
"Right column final paragraph. Verifies column-aware layout.",
|
|
]
|
|
y = 170
|
|
for p in paras_right:
|
|
rect = fitz.Rect(right_x0, y, right_x1, y + 80)
|
|
page.insert_textbox(rect, p, fontsize=10)
|
|
y += 100
|
|
|
|
doc.save(str(path))
|
|
doc.close()
|
|
|
|
|
|
def translate_with_mock(input_path: Path, output_path: Path) -> None:
|
|
"""Translate using a mock that returns the same text (identity)."""
|
|
from translators.pdf_translator import PDFTranslator
|
|
translator = PDFTranslator()
|
|
def _flex(*args, **kwargs):
|
|
if args and hasattr(args[0], '__iter__') and not isinstance(args[0], str):
|
|
first = args[0][0] if len(args[0]) else None
|
|
if first is not None and hasattr(first, 'text'):
|
|
class _R:
|
|
def __init__(self, t): self.translated_text = t
|
|
return [_R(r.text) for r in args[0]]
|
|
return list(args[0]) if args else []
|
|
mock = MagicMock()
|
|
mock.__class__.__name__ = "MagicMock"
|
|
mock.translate_batch = MagicMock(side_effect=_flex)
|
|
translator.set_provider(mock)
|
|
translator.translate_file(input_path, output_path,
|
|
target_language='fr', source_language='en')
|
|
|
|
|
|
def inspect_columns(pdf_path: Path) -> dict:
|
|
"""Count blocks per column and check no cross-column overlap."""
|
|
doc = fitz.open(str(pdf_path))
|
|
page = doc[0]
|
|
blocks = [b for b in page.get_text("dict").get("blocks", [])
|
|
if b.get("type") == 0]
|
|
left_blocks = [b for b in blocks if b["bbox"][0] < 200]
|
|
right_blocks = [b for b in blocks if b["bbox"][0] >= 200]
|
|
doc.close()
|
|
return {
|
|
"total": len(blocks),
|
|
"left": len(left_blocks),
|
|
"right": len(right_blocks),
|
|
}
|
|
|
|
|
|
def render(pdf_path: Path, out_png: Path) -> None:
|
|
doc = fitz.open(str(pdf_path))
|
|
pix = doc[0].get_pixmap(matrix=fitz.Matrix(1.5, 1.5))
|
|
pix.save(str(out_png))
|
|
doc.close()
|
|
|
|
|
|
def main():
|
|
print("B3.8 multi-column verification")
|
|
print("=" * 60)
|
|
|
|
in_pdf = OUT_DIR / 'journal_in.pdf'
|
|
out_pdf = OUT_DIR / 'journal_out.pdf'
|
|
in_png = OUT_DIR / 'journal_in.png'
|
|
out_png = OUT_DIR / 'journal_out.png'
|
|
|
|
print(f"[1] Building 2-column journal PDF -> {in_pdf.name}")
|
|
build_2col_journal_pdf(in_pdf)
|
|
|
|
print(f"[2] Translating with B3.8 enabled -> {out_pdf.name}")
|
|
translate_with_mock(in_pdf, out_pdf)
|
|
|
|
print(f"[3] Inspecting input column distribution...")
|
|
in_info = inspect_columns(in_pdf)
|
|
print(f" input : total={in_info['total']} left={in_info['left']} right={in_info['right']}")
|
|
|
|
print(f"[4] Inspecting output column distribution...")
|
|
out_info = inspect_columns(out_pdf)
|
|
print(f" output : total={out_info['total']} left={out_info['left']} right={out_info['right']}")
|
|
|
|
print(f"[5] Rendering both to PNG...")
|
|
render(in_pdf, in_png)
|
|
render(out_pdf, out_png)
|
|
print(f" input : {in_png}")
|
|
print(f" output : {out_png}")
|
|
|
|
print("=" * 60)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|