File size: 5,635 Bytes
58e6885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""
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()