fix(magazine): dotted leaders and thin rules are not reported as missed text

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-28 17:34:06 +02:00
parent 19af236bcd
commit 32a23a237a
3 changed files with 48 additions and 0 deletions

View File

@ -38,6 +38,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **Articles nobody has reviewed are blue.** In the article list an unapproved, unrejected article has a blue background and edge and a *not reviewed* pill (approved keeps its green edge), so what still needs a person stands out.
- **Overlapping boxes are fixed automatically.** Where a lower-ranked box runs over a higher-ranked one (headline > deck/byline > body > caption/sidebar/pull quote), the lower one is cut back to the largest part that no longer overlaps, unless that would leave under 40% of it (then both stay for a human). Hand-placed boxes and boxes of equal rank are never touched.
- **Fewer false alarms and better fits:** a strip of solid ink beside a headline (a photo or filled panel behind it) is no longer counted as letters cut off; headline and deck boxes may grow up to 70 px sideways (body boxes 16 px); an inset pull-quote box that repeats text a box already holds becomes a duplicate.
- **Rules and dotted leaders are not missed text.** The dots between a table-of-contents entry and its page number (and thin printed rules) are ink but no letters; the audit now ignores ink that is only a few pixel rows tall.
- **Unboxed ink in or beside a picture, chart or advert is no longer reported as missed text** (labels of a chart, text set into a photo, an advert's own wording).
### Fixed

View File

@ -2878,6 +2878,21 @@ _AUDIT_GRAPHIC_MARGIN = 10.0 # points: unboxed ink this close to a picture/ch
_AUDIT_CLIP_STRIP_PX = 14 # how far outside a box edge to look for ink that belongs to it (~1-2 letters)
_RULE_MAX_INK_ROWS = 6 # pixel rows with ink: a rule or dotted leader is a few, a text line is 8+
def _is_rule_only(pixels, blob: Sequence[float], page_meta: dict[str, Any]) -> bool:
"""True when the ink inside `blob` is only a thin rule or a dotted leader (the dots between a
table-of-contents entry and its page number), which has no letters and nothing to narrate."""
height, width = pixels.shape
sx, sy = width / max(1.0, float(page_meta["width"])), height / max(1.0, float(page_meta["height"]))
x, y, w, h = blob
region = pixels[max(0, int(y * sy)):int((y + h) * sy) + 1, max(0, int(x * sx)):int((x + w) * sx) + 1]
if region.size == 0:
return False
return int((region.sum(axis=1) >= 3).sum()) <= _RULE_MAX_INK_ROWS
def _clipped_edges(pixels, page_meta: dict[str, Any], text_blocks: list[dict[str, Any]],
boxes: list[Sequence[float]]) -> list[dict[str, Any]]:
"""Text boxes whose edge cuts through text: ink in a thin strip just outside a side, ignoring
@ -3534,6 +3549,8 @@ def audit_page_coverage(doc_dir: Path, layout: dict[str, Any], page_no: int) ->
# graphic, not narratable text a box forgot: a blob in or beside one is not reported.
if any(bbox_overlaps(blob, g, tol=_AUDIT_GRAPHIC_MARGIN) for g in graphic_boxes):
continue
if pixels is not None and _is_rule_only(pixels, blob, page_meta):
continue # a printed rule or dotted leader, not letters
kept_cells += len(rows)
report["uncovered"].append(blob)
report["uncovered_ratio"] = round(kept_cells / total, 4)

View File

@ -3269,3 +3269,33 @@ class ClippedEdgeIgnoresSolidBackgroundsTests(unittest.TestCase):
def test_text_running_past_the_edge_is_clipping_but_a_solid_panel_is_not(self):
self.assertEqual([c["side"] for c in self.audit(False)], ["right"])
self.assertEqual(self.audit(True), [])
class LeaderLineIsNotMissedTextTests(unittest.TestCase):
def audit(self, draw_fn):
from pathlib import Path
from PIL import Image, ImageDraw
from core.magazine import audit_page_coverage
d = Path(tempfile.mkdtemp(prefix="ttvc-rule-"))
self.addCleanup(shutil.rmtree, d, True)
(d / "pages").mkdir()
im = Image.new("RGB", (600, 800), (248, 242, 244))
draw_fn(ImageDraw.Draw(im))
im.save(d / "pages" / "0001.png")
layout = {"pages": [{"page": 1, "width": 600, "height": 800}], "images": [], "blocks": []}
return audit_page_coverage(d, layout, 1)["uncovered"]
def test_a_dotted_leader_or_rule_is_not_reported_but_text_lines_are(self):
def leader(draw):
for x in range(200, 400, 6): # dots between a heading and its page number
draw.rectangle([x, 300, x + 1, 302], fill=(93, 91, 92))
self.assertEqual(self.audit(leader), [])
def rule(draw):
draw.rectangle([100, 500, 400, 502], fill=(93, 91, 92))
self.assertEqual(self.audit(rule), [])
def text(draw):
for y in range(300, 360, 12):
draw.rectangle([100, y, 300, y + 8], fill=(93, 91, 92))
self.assertEqual(len(self.audit(text)), 1)