feat(format): B3.6+B3.7 — PDF transparent redaction + next-block-aware layout
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m36s

B3.6 — fix two visual bugs reported on the user's prod PDF:
  1. Title 'Spécification technique : Office Translator v3.0' overflowed
     its 2-line bbox and overlapped the 'Version du document...' block.
     Root cause: MAX_VERTICAL_EXPANSION was 1.5x the original height,
     way too small for a long French title. Bumped to 6.0x.
  2. 'Avis important' blue background had white rectangular patches.
     Root cause: redaction always used fill=(1,1,1) (opaque white),
     which erased the colored drawing underneath the text.
     Fix: detect when a block's bbox intersects a page drawing,
     and use fill=None (transparent) for the redaction in that case.
     The original drawing survives intact.

B3.7 — eliminate remaining block-vs-next-block overlap:
  Computes each block's 'next_block_y' (the y0 of the nearest block
  below it on the same page) and uses it as the ceiling for vertical
  expansion. Previously the smart-fit logic used the page bottom as
  the ceiling, which let long translated blocks flow into their
  neighbour (e.g. 'Pour la dernière version...' overlapping
  '8. Résolution des problèmes' in the TOC).

Also includes:
  - 9 new tests (6 B3.6 + 3 B3.7) — total 446 tests pass, zero regression
  - scripts/verify_b3_6_fix.py — visual+structural verification
  - Updated sample_files/test_corpus/test_pdf_translated.pdf with the
    clean B3.6+B3.7 output
This commit is contained in:
2026-07-14 18:56:31 +02:00
parent e706cef5d6
commit 3ae28dd3cb
6 changed files with 721 additions and 27 deletions

View File

@@ -0,0 +1,45 @@
"""Reproduce the image issues: title overflow + white box on colored block."""
import sys
sys.path.insert(0, '.')
import fitz
from pathlib import Path
# Open the translated PDF from the user
src = Path('sample_files/test_corpus/test_pdf_translated (1).pdf')
doc = fitz.open(str(src))
# Page 1
p = doc[0]
print("=== Page 1 text blocks ===")
for i, b in enumerate(p.get_text("dict").get("blocks", [])):
bbox = b.get("bbox", (0, 0, 0, 0))
if b.get("type") != 0: # not text
continue
text_lines = []
for line in b.get("lines", []):
for span in line.get("spans", []):
text_lines.append(span.get("text", ""))
text = " ".join(text_lines)[:60]
print(f" [{i}] bbox=({bbox[0]:.0f},{bbox[1]:.0f},{bbox[2]:.0f},{bbox[3]:.0f}) text={text!r}")
print()
print("=== Page 1 drawings ===")
for i, d in enumerate(p.get_drawings()):
rect = d.get("rect")
fill = d.get("fill")
color = d.get("color")
print(f" [{i}] rect=({rect.x0:.0f},{rect.y0:.0f},{rect.x1:.0f},{rect.y1:.0f}) fill={fill} color={color}")
# Open the original to compare
orig = fitz.open('sample_files/test_corpus/test_pdf.pdf')
p_orig = orig[0]
print()
print("=== Original Page 1 drawings (for comparison) ===")
for i, d in enumerate(p_orig.get_drawings()):
rect = d.get("rect")
fill = d.get("fill")
color = d.get("color")
print(f" [{i}] rect=({rect.x0:.0f},{rect.y0:.0f},{rect.x1:.0f},{rect.y1:.0f}) fill={fill} color={color}")
doc.close()
orig.close()

228
scripts/verify_b3_6_fix.py Normal file
View File

@@ -0,0 +1,228 @@
"""
B3.6 visual verification script.
Steps:
1. Re-translate the test PDF using a mock provider (cheap, no API cost).
2. Render both the OLD broken PDF and the NEW (B3.6) PDF as PNGs.
3. Inspect drawings: the blue "Avis important" box must still be present
in the new PDF but was lost in the old PDF.
4. Inspect title block: it must not overlap the next block ("Version du document...").
5. Open both PNGs side-by-side so we can eyeball the difference.
Usage: python scripts/verify_b3_6_fix.py
"""
import sys
import os
from pathlib import Path
from unittest.mock import MagicMock
# Make repo root importable
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import fitz # PyMuPDF
REPO_ROOT = Path(__file__).resolve().parent.parent
OLD_PDF = REPO_ROOT / "sample_files" / "test_corpus" / "test_pdf_translated (1).pdf"
ORIG_PDF = REPO_ROOT / "sample_files" / "test_corpus" / "test_pdf.pdf"
NEW_PDF = REPO_ROOT / "sample_files" / "test_corpus" / "test_pdf_translated_fresh.pdf"
RENDER_DIR = REPO_ROOT / "scripts" / "_renders"
RENDER_DIR.mkdir(parents=True, exist_ok=True)
def _render_pdf_page(pdf_path: Path, page_num: int = 0, out_png: Path = None,
zoom: float = 1.5) -> Path:
"""Render a single PDF page as PNG."""
doc = fitz.open(str(pdf_path))
page = doc[page_num]
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
if out_png is None:
out_png = RENDER_DIR / f"{pdf_path.stem}_p{page_num + 1}.png"
pix.save(str(out_png))
doc.close()
return out_png
def _translate_with_mock(input_path: Path, output_path: Path) -> Path:
"""Translate using a mock provider that returns the French text we want."""
from translators.pdf_translator import PDFTranslator
translator = PDFTranslator()
# Mock provider: a fake French translation that's longer than the English original
# (this is the realistic case that triggered the overflow bug)
def mock_translate(text, target_language, source_language):
# Return a slightly longer French version to stress the layout
if not text:
return text
# Replace common patterns with longer French to trigger overflow
replacements = {
"Technical Specification": "Spécification technique : Office Translator",
"Document Version": "Version du document",
"Table of Contents": "Table des matières",
"1. Introduction": "1. Introduction",
"2. Installation and Configuration": "2. Installation et configuration",
"3. Translation Engine": "3. Moteur de traduction",
"4. Format Preservation": "4. Préservation des formats",
"5. Quality Assurance": "5. Assurance qualité",
"6. Performance and Scaling": "6. Performances et mise à l'échelle",
"7. API Reference": "7. Référence API",
"8. Troubleshooting": "8. Résolution des problèmes",
"Important Notice": "Avis important",
"This is a preliminary document.": "Ceci est un document préliminaire.",
"Subject to change without notice.": "Sujet à changement sans préavis.",
"For the latest version of this document, visit:": "Pour la dernière version de ce document, visitez :",
"http://docs.translator.example.com/v3": "http://docs.translator.example.com/v3",
}
result = text
for eng, fr in replacements.items():
result = result.replace(eng, fr)
return result
mock = MagicMock()
mock.translate_batch = MagicMock(side_effect=lambda texts, tgt, src: [mock_translate(t, tgt, src) for t in texts])
translator.set_provider(mock)
translator.translate_file(
input_path, output_path,
target_language="fr",
source_language="en",
)
return output_path
def _inspect_drawings(pdf_path: Path) -> dict:
"""Inspect page 1: count blue background drawings (the "Avis important" box)."""
doc = fitz.open(str(pdf_path))
page = doc[0]
drawings = page.get_drawings()
# The "Avis important" box has fill (0.85, 0.92, 1.0) — light blue
blue_boxes = []
for d in drawings:
fill = d.get("fill")
if fill is None:
continue
# Match light blue (R=0.85, G=0.92, B=1.0)
if all(abs(fill[i] - target) < 0.05 for i, target in enumerate([0.85, 0.92, 1.0])):
blue_boxes.append(d)
doc.close()
return {
"total_drawings": len(drawings),
"blue_boxes": len(blue_boxes),
"blue_rects": [(round(d["rect"].x0, 1), round(d["rect"].y0, 1),
round(d["rect"].x1, 1), round(d["rect"].y1, 1))
for d in blue_boxes],
}
def _inspect_title_overlap(pdf_path: Path) -> dict:
"""Check if the title block bottom overlaps the next block top."""
doc = fitz.open(str(pdf_path))
page = doc[0]
text_dict = page.get_text("dict")
# Find the title block (largest font on the page)
title_block = None
next_block = None
sorted_blocks = sorted(
[b for b in text_dict.get("blocks", []) if b.get("type") == 0],
key=lambda b: -(
max((s.get("size", 12) for line in b.get("lines", [])
for s in line.get("spans", [])), default=12)
)
)
if sorted_blocks:
title_block = sorted_blocks[0]
title_text = " ".join(
s.get("text", "")
for line in title_block.get("lines", [])
for s in line.get("spans", [])
)
title_bottom = title_block["bbox"][3]
# Find the next block right below the title
candidates = [
b for b in text_dict.get("blocks", [])
if b.get("type") == 0
and b is not title_block
and b["bbox"][1] >= title_bottom - 5
and b["bbox"][1] <= title_bottom + 50
]
if candidates:
next_block = min(candidates, key=lambda b: b["bbox"][1])
result = {
"title": " ".join(
s.get("text", "")
for line in (title_block or {}).get("lines", [])
for s in line.get("spans", [])
)[:60],
"title_bbox": title_block["bbox"] if title_block else None,
}
if next_block:
next_text = " ".join(
s.get("text", "")
for line in next_block.get("lines", [])
for s in line.get("spans", [])
)[:60]
result["next_text"] = next_text
result["next_bbox"] = next_block["bbox"]
result["overlap_px"] = round(title_block["bbox"][3] - next_block["bbox"][1], 2)
result["overlaps"] = result["overlap_px"] > 2
doc.close()
return result
def main():
print("=" * 60)
print("B3.6 VERIFICATION — visual & structural")
print("=" * 60)
# 1. Translate the original with B3.6 fix
print("\n[1] Re-translating test_pdf.pdf with B3.6 fix...")
if not ORIG_PDF.exists():
print(f" ERROR: original PDF not found at {ORIG_PDF}")
return 1
_translate_with_mock(ORIG_PDF, NEW_PDF)
print(f" wrote {NEW_PDF.name}")
# 2. Render both PDFs as PNG for visual comparison
print("\n[2] Rendering PDFs to PNG...")
old_png = _render_pdf_page(OLD_PDF)
new_png = _render_pdf_page(NEW_PDF)
orig_png = _render_pdf_page(ORIG_PDF)
print(f" OLD broken: {old_png.relative_to(REPO_ROOT)}")
print(f" NEW (B3.6): {new_png.relative_to(REPO_ROOT)}")
print(f" ORIGINAL ref: {orig_png.relative_to(REPO_ROOT)}")
# 3. Inspect drawings - blue box preservation
print("\n[3] Inspecting drawings (blue 'Avis important' box)...")
for label, path in [("OLD", OLD_PDF), ("NEW (B3.6)", NEW_PDF), ("ORIGINAL", ORIG_PDF)]:
info = _inspect_drawings(path)
verdict = "[OK]" if info["blue_boxes"] >= 1 else "[FAIL]"
print(f" {verdict} {label:12} -> {info['blue_boxes']} blue box(es) of "
f"{info['total_drawings']} total drawings")
# 4. Inspect title overlap
print("\n[4] Inspecting title-vs-next-block overlap...")
for label, path in [("OLD", OLD_PDF), ("NEW (B3.6)", NEW_PDF)]:
info = _inspect_title_overlap(path)
if "overlaps" in info:
verdict = "[FAIL OVERLAPS]" if info["overlaps"] else "[OK no overlap]"
print(f" {verdict} {label:12} -> title={info['title']!r:40} "
f"overlap_px={info['overlap_px']:+.1f}")
else:
print(f" [?] {label:12} -> could not determine (insufficient blocks)")
print("\n" + "=" * 60)
print("Visual comparison rendered to:")
print(f" {old_png}")
print(f" {new_png}")
print(f" {orig_png}")
print("=" * 60)
return 0
if __name__ == "__main__":
sys.exit(main())