Spaces:
Sleeping
Sleeping
| """ | |
| Scan all final-infographic SVGs from a quality-check run and look for | |
| inconsistent shrink ratios inside the title block. | |
| For each <g data-type="title"> we collect every <text> element. If a text | |
| carries both font-size="<N>px" (the original attribute) and inline | |
| style="font-size: <M>px;" (rewritten by shrink_overlapping_text() in | |
| screenshot_utils.py), we treat M/N as that text's shrink ratio. | |
| A title block is "inconsistently shrunk" when at least two of its texts | |
| were touched by the shrinker AND their ratios differ. That's exactly the | |
| case the collaborator described: the JS picks per-text targets, so two | |
| title segments end up scaled by different factors. | |
| """ | |
| import argparse | |
| import csv | |
| import os | |
| import re | |
| from pathlib import Path | |
| from typing import List, Tuple | |
| TITLE_BLOCK_RE = re.compile( | |
| r'<g[^>]*data-type="title"[^>]*>(?P<inner>.*?)</g>', | |
| re.DOTALL, | |
| ) | |
| TEXT_RE = re.compile(r"<text\b[^>]*>", re.IGNORECASE) | |
| ATTR_FS_RE = re.compile(r'font-size="(?P<v>[\d.]+)px"') | |
| STYLE_FS_RE = re.compile(r'style="[^"]*font-size:\s*(?P<v>[\d.]+)\s*px[^"]*"') | |
| def extract_title_texts(svg: str) -> List[Tuple[float, float]]: | |
| """Return [(attr_font_px, inline_font_px), ...] for every <text> in | |
| the (first) title block, omitting texts that lack either field.""" | |
| m = TITLE_BLOCK_RE.search(svg) | |
| if not m: | |
| return [] | |
| inner = m.group("inner") | |
| out = [] | |
| for tm in TEXT_RE.finditer(inner): | |
| tag = tm.group(0) | |
| a = ATTR_FS_RE.search(tag) | |
| s = STYLE_FS_RE.search(tag) | |
| if not a: | |
| continue | |
| attr = float(a.group("v")) | |
| inline = float(s.group("v")) if s else attr # not shrunk -> ratio 1.0 | |
| out.append((attr, inline)) | |
| return out | |
| def parse_args(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--root", default="output/quality_check", | |
| help="Directory containing per-template subdirs") | |
| p.add_argument("--out", default="output/quality_check/_title_shrink_audit.csv") | |
| p.add_argument("--ratio-tol", type=float, default=0.01, | |
| help="Treat shrink ratios within this absolute tolerance as 'same'") | |
| return p.parse_args() | |
| def main(): | |
| args = parse_args() | |
| root = Path(args.root) | |
| rows = [] | |
| inconsistent = [] | |
| n_title_blocks = 0 | |
| n_with_shrink = 0 | |
| n_inconsistent = 0 | |
| n_files = 0 | |
| n_no_title = 0 | |
| for tpl_dir in sorted(p for p in root.iterdir() if p.is_dir()): | |
| chart_name = tpl_dir.name | |
| for svg_path in sorted(tpl_dir.glob("*.svg")): | |
| n_files += 1 | |
| try: | |
| svg = svg_path.read_text(encoding="utf-8", errors="ignore") | |
| except OSError: | |
| continue | |
| texts = extract_title_texts(svg) | |
| if not texts: | |
| n_no_title += 1 | |
| continue | |
| n_title_blocks += 1 | |
| ratios = [round(inl / attr, 4) for attr, inl in texts if attr > 0] | |
| # only count texts whose attr differs from inline (i.e. actually | |
| # touched by shrinker) | |
| shrunk_ratios = [r for r in ratios if abs(r - 1.0) > args.ratio_tol] | |
| if not shrunk_ratios: | |
| continue | |
| n_with_shrink += 1 | |
| # distinct shrink ratios | |
| distinct = sorted({round(r, 3) for r in ratios}) | |
| is_inconsistent = ( | |
| len([r for r in distinct if abs(r - 1.0) > args.ratio_tol]) >= 1 | |
| and len(distinct) >= 2 | |
| ) | |
| if is_inconsistent: | |
| n_inconsistent += 1 | |
| row = { | |
| "chart_name": chart_name, | |
| "svg": str(svg_path.relative_to(root.parent)), | |
| "n_texts": len(texts), | |
| "attrs": ";".join(f"{a:.1f}" for a, _ in texts), | |
| "inlines": ";".join(f"{i:.2f}" for _, i in texts), | |
| "ratios": ";".join(f"{r:.3f}" for r in ratios), | |
| "min_ratio": min(ratios) if ratios else 1.0, | |
| "max_ratio": max(ratios) if ratios else 1.0, | |
| "ratio_spread": (max(ratios) - min(ratios)) if ratios else 0.0, | |
| "inconsistent": is_inconsistent, | |
| } | |
| rows.append(row) | |
| if is_inconsistent: | |
| inconsistent.append(row) | |
| os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True) | |
| rows.sort(key=lambda r: (-r["ratio_spread"], r["chart_name"])) | |
| with open(args.out, "w", newline="", encoding="utf-8") as fh: | |
| w = csv.DictWriter(fh, fieldnames=list(rows[0].keys()) if rows else [ | |
| "chart_name", "svg", "n_texts", "attrs", "inlines", "ratios", | |
| "min_ratio", "max_ratio", "ratio_spread", "inconsistent", | |
| ]) | |
| w.writeheader() | |
| for r in rows: | |
| w.writerow(r) | |
| print(f"Wrote {args.out}") | |
| print() | |
| print(f"Total final-svg files scanned: {n_files}") | |
| print(f" with a <g data-type='title'> block: {n_title_blocks} (no title: {n_no_title})") | |
| print(f" in which >=1 text was shrunk: {n_with_shrink}") | |
| print(f" with INCONSISTENT shrink ratios: {n_inconsistent}" | |
| f" ({n_inconsistent/max(n_with_shrink,1)*100:.1f}% of shrunk)") | |
| print() | |
| print("=== top 15 worst (largest ratio spread inside the title block) ===") | |
| print(f"{'spread':>7s} {'min':>5s} {'max':>5s} {'n':>2s} chart_name / svg") | |
| for r in rows[:15]: | |
| print( | |
| f" {r['ratio_spread']:.3f} {r['min_ratio']:.3f} {r['max_ratio']:.3f} " | |
| f"{r['n_texts']:>2d} {r['chart_name']} {os.path.basename(r['svg'])}" | |
| ) | |
| if __name__ == "__main__": | |
| main() | |