#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Розгортка «клітинка N pt» для клікабельних областей на малюнку стор. 16.

Що це. У варіанті Б (`verify/highlight_test.py`) підсвітка полів складається з
клітинок 6 pt, і на самій заливці видно шов: сусідні клітинки обрізають свій
потік вигляду по `/BBox`, а фарба напівпрозора (`ExtGState /ca 0.45`), тож на
стику кожен піксель фарбується двічі неповним покриттям і виходить світліша
сітка.

Лікування — дві зміни разом:

* **непрозора фарба**: колір заздалегідь змішаний з білим
  (`c' = 0.45·c + 0.55·1`), `ExtGState` не потрібен узагалі. Подвійне
  накладання непрозорого кольору дає той самий колір;
* **перекриття клітинок** на чверть кроку (по 1/8 кроку з кожного боку), щоб
  на стику не лишалося пікселя, який жодна клітинка не накриває цілком.

Пастка, яку це створює: непрозора **векторна** заливка сховала б чорні штрихи
всередині області — складки одягу, риси обличчя. Тому форму беремо не з
контуру, а з **растрового трафарету** варіанта В: заливка по пікселях від
чорних ліній малюнка, лінії в трафарет не входять і лишаються прозорими
дірками. Кожна клітинка малює свій шматок того самого трафарету, обрізаний
власним `/BBox`.

Трафарет один на область і **спільний для всіх клітинок**: `/ImageMask`
лежить окремим об'єктом, а потік вигляду клітинки — це 70 байтів
«колір + `cm` + `/Im0 Do`» зі спільним словником `/Resources`. Шеринг дає
рівно **вдвічі**: 445 Б на клітинку проти 850 Б, якщо в кожної свій трафарет.

Виміряно на стор. 16 (11 областей, сума площ контурів 5 791 pt²):

| клітинка | віджетів | +КБ на малюнок | кліки в ціль | «нічийне» | збіг форми | обхід форми |
|---|---|---|---|---|---|---|
| 1 pt | 6 165 | +2 728 | 99.6 % | 1.5 % | 94 % | 120.6 с |
| 2 pt | 1 615 |  +702 | 98.9 % | 3.8 % | 86 % | 8.8 с |
| 3 pt |   763 |  +337 | 98.1 % | 6.2 % | 79 % | 2.0 с |
| 4 pt |   430 |  +185 | 98.5 % | 6.4 % | 78 % | 0.7 с |
| 6 pt |   216 |   +95 | 96.1 % | 10.4 % | 68 % | 0.2 с |
| В (11 прямокутників) | 11 | +8 | 96.7 % | 16.9 % | 58 % | 0.002 с |

**Обхід форми росте квадратично**, і це, а не вага, вбиває 1 pt: рендер
сторінки лишається 0.08 с, а побудова переліку полів — дві хвилини. У PyMuPDF
причина в тому, що кожен нащадок змушує наново прочитати `/Kids` батька (їх
6 165). Чи так само поводиться Acrobat — файлом не перевірити, тільки руками.

Запуск:
    .venv/bin/python verify/highlight_grid.py
    .venv/bin/python verify/highlight_grid.py --sizes 1,2,3,4,6 --study 2 \\
        --noshare 1,2 --walk
"""
import argparse
import json
import math
import os
import re
import sys
import time

import fitz

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import highlight_test as HT                      # noqa: E402

SRC = "forms/Level2_UA_form_v06.pdf"
JSON_PATH = "editor/regions_p16_q2.json"
PNO = 15
OUTDIR = "verify"
OUT = "verify/highlight_test_v02.pdf"
ALPHA = 0.45                   # у який колір «змішати з білим»
COVER = 0.20                   # поріг покриття клітинки контуром
OVERLAP = 0.25                 # перекриття сусідніх клітинок, часток кроку
SUB = 4
FONT = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
SIZES = [1.0, 2.0, 3.0, 4.0, 6.0]


# ---------------------------------------------------------------- дрібне
def mix(c, a):
    """Непрозорий еквівалент напівпрозорої фарби на білому папері."""
    return tuple(a * v + (1.0 - a) for v in c)


def fmt(n):
    s = "%d" % n
    out = ""
    while len(s) > 3:
        out = " " + s[-3:] + out
        s = s[:-3]
    return s + out


def form_xobj_res(doc, w, h, res, content):
    """Form XObject зі СПІЛЬНИМ (непрямим) словником ресурсів."""
    x = doc.get_new_xref()
    doc.update_object(
        x, "<</Type/XObject/Subtype/Form/FormType 1/BBox[0 0 %.3f %.3f]"
           "/Resources %d 0 R>>" % (w, h, res))
    doc.update_stream(x, content.encode("latin-1"), new=True)
    return x


def cells_of(reg, step, thr):
    """Клітинки сітки, які лишаються за областю (та сама логіка, що в Б).

    Порахований scanline'ом по підсітці step/4 — при кроці 1 pt поточковий
    `inside()` рахувався б хвилинами.
    """
    rb = HT.bbox(reg)
    ix0 = int(math.floor(rb.x0 / step))
    iy0 = int(math.floor(rb.y0 / step))
    ncx = int(math.ceil(rb.x1 / step)) - ix0 + 1
    ncy = int(math.ceil(rb.y1 / step)) - iy0 + 1
    w, h = ncx * SUB, ncy * SUB
    m = HT.scan_mask(reg, ix0 * step, iy0 * step, w, h, step / SUB)
    need = thr * SUB * SUB
    out = []
    for cy in range(ncy):
        for cx in range(ncx):
            n = 0
            for jy in range(SUB):
                row = (cy * SUB + jy) * w + cx * SUB
                n += m[row] + m[row + 1] + m[row + 2] + m[row + 3]
            if n >= need:
                x0, y0 = (ix0 + cx) * step, (iy0 + cy) * step
                out.append(fitz.Rect(x0, y0, x0 + step, y0 + step))
    return out


# ---------------------------------------------------------------- трафарети
def build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale, translucent=False):
    """Один `/ImageMask` на область + один спільний `/Resources` на область."""
    st = []
    gs = ("/ExtGState<<" + (HT.GS % (ALPHA, ALPHA)) + ">>") if translucent else ""
    for reg in regs:
        mask, mrect, mw, mh = HT.raster_mask(reg, ink_ok, pw, ph, ox, oy, scale)
        im = HT.image_mask_xobj(doc, mask, mw, mh)
        res = doc.get_new_xref()
        doc.update_object(res, "<</XObject<</Im0 %d 0 R>>%s>>" % (im, gs))
        st.append((im, res, mrect, sum(mask) * scale * scale, mask, mw, mh))
    return st


def paint_op(reg, mrect, box, translucent=False):
    """Вміст потоку: колір + трафарет, зсунутий під `/BBox` клітинки.

    Основний режим — **непрозорий**: колір заздалегідь змішано з білим, тож
    подвійне накладання на стику дає той самий колір. Напівпрозорий лишено
    для діагностики (саме він дає шов).
    """
    c = HT.hex2rgb(reg["color"])
    op = "/GSa gs " if translucent else ""
    r, g, b = c if translucent else mix(c, ALPHA)
    return ("q %s%.3f %.3f %.3f rg %.3f 0 0 %.3f %.3f %.3f cm /Im0 Do Q"
            % (op, r, g, b, mrect.width, mrect.height,
               mrect.x0 - box.x0, box.y1 - mrect.y1))


# ---------------------------------------------------------------- сторінки
def add_grid(doc, regs, st, step, prefix, overlap=OVERLAP, translucent=False):
    """Клітинки step pt з перекриттям; одне поле на область, клітинки — `/Kids`."""
    annots, fields, layer = [], [], []
    e = step * overlap / 2.0
    w = step + 2 * e
    off = HT.form_xobj(doc, w, w, "", "")          # порожній `Off`, спільний
    per = []
    for i, reg in enumerate(regs, 1):
        im, res, mrect = st[i - 1][:3]
        kids = []
        for c in cells_of(reg, step, COVER):
            box = fitz.Rect(c.x0 - e, c.y0 - e, c.x1 + e, c.y1 + e)
            on = form_xobj_res(doc, box.width, box.height, res,
                               paint_op(reg, mrect, box, translucent))
            kids.append((HT.widget_obj(doc, box, on, off), box))
        if not kids:
            box = HT.bbox(reg)
            on = form_xobj_res(doc, box.width, box.height, res,
                               paint_op(reg, mrect, box, translucent))
            kids = [(HT.widget_obj(doc, box, on, HT.form_xobj(
                doc, box.width, box.height, "", "")), box)]
        par = doc.get_new_xref()
        doc.update_object(par, "<</FT/Btn/T(%s_art%02d)/V/Off/Kids[%s]>>"
                          % (prefix, i, " ".join("%d 0 R" % k for k, _ in kids)))
        for k, box in kids:
            doc.xref_set_key(k, "Parent", "%d 0 R" % par)
            annots.append(k)
            layer.append((box, i - 1))
        fields.append(par)
        per.append((reg["name"], len(kids)))
    return annots, fields, layer, per


def add_grid_noshare(doc, regs, st, step, prefix, overlap=OVERLAP):
    """Те саме, але БЕЗ шерингу: у кожної клітинки свій трафарет і свої ресурси.

    Потрібне тільки щоб виміряти, скільки дає шеринг. Трафарет ріжеться по
    прямокутнику клітинки, тож на 1 pt це 4×4 пікселі — але кожен зі своїм
    словником об'єкта.
    """
    annots, fields, layer, per = [], [], [], []
    e = step * overlap / 2.0
    for i, reg in enumerate(regs, 1):
        mrect, mask, mw, mh = st[i - 1][2], st[i - 1][4], st[i - 1][5], st[i - 1][6]
        sc = mrect.width / mw
        r, g, b = mix(HT.hex2rgb(reg["color"]), ALPHA)
        kids = []
        for c in cells_of(reg, step, COVER):
            box = fitz.Rect(c.x0 - e, c.y0 - e, c.x1 + e, c.y1 + e)
            x0 = max(0, int((box.x0 - mrect.x0) / sc))
            x1 = min(mw, int(math.ceil((box.x1 - mrect.x0) / sc)))
            y0 = max(0, int((mrect.y1 - box.y1) / sc))
            y1 = min(mh, int(math.ceil((mrect.y1 - box.y0) / sc)))
            cw, ch = max(1, x1 - x0), max(1, y1 - y0)
            sub = bytearray(cw * ch)
            for iy in range(ch):
                src = (y0 + iy) * mw + x0
                dst = iy * cw
                for ix in range(cw):
                    if y0 + iy < mh and x0 + ix < mw:
                        sub[dst + ix] = mask[src + ix]
            im = HT.image_mask_xobj(doc, sub, cw, ch)
            crect = fitz.Rect(mrect.x0 + x0 * sc, mrect.y1 - y1 * sc,
                              mrect.x0 + x1 * sc, mrect.y1 - y0 * sc)
            on = HT.form_xobj(
                doc, box.width, box.height, "/XObject<</Im0 %d 0 R>>" % im,
                "q %.3f %.3f %.3f rg %.3f 0 0 %.3f %.3f %.3f cm /Im0 Do Q"
                % (r, g, b, crect.width, crect.height,
                   crect.x0 - box.x0, box.y1 - crect.y1))
            off = HT.form_xobj(doc, box.width, box.height, "", "")
            kids.append((HT.widget_obj(doc, box, on, off), box))
        par = doc.get_new_xref()
        doc.update_object(par, "<</FT/Btn/T(%s_art%02d)/V/Off/Kids[%s]>>"
                          % (prefix, i, " ".join("%d 0 R" % k for k, _ in kids)))
        for k, box in kids:
            doc.xref_set_key(k, "Parent", "%d 0 R" % par)
            annots.append(k)
            layer.append((box, i - 1))
        fields.append(par)
        per.append((reg["name"], len(kids)))
    return annots, fields, layer, per


def add_rects(doc, regs, st, prefix):
    """Контрольний варіант В: один прямокутник (bbox контуру) на область."""
    annots, fields, layer, per = [], [], [], []
    for i, reg in enumerate(regs, 1):
        im, res, mrect = st[i - 1][:3]
        box = HT.bbox(reg)
        on = form_xobj_res(doc, box.width, box.height, res,
                           paint_op(reg, mrect, box))
        off = HT.form_xobj(doc, box.width, box.height, "", "")
        x = HT.widget_obj(doc, box, on, off,
                          "/FT/Btn/T(%s_art%02d)/V/Off" % (prefix, i))
        annots.append(x)
        fields.append(x)
        layer.append((box, i - 1))
        per.append((reg["name"], 1))
    return annots, fields, layer, per


def attach(doc, page, annots, fields, mark_all=False):
    doc.xref_set_key(page.xref, "Annots",
                     "[%s]" % " ".join("%d 0 R" % x for x in annots))
    if mark_all:
        for x in annots:
            doc.xref_set_key(x, "AS", "/Yes")
        for x in fields:
            doc.xref_set_key(x, "V", "/Yes")


def acroform(doc, fields):
    doc.xref_set_key(doc.pdf_catalog(), "AcroForm",
                     "<</Fields[%s]/NeedAppearances false>>"
                     % " ".join("%d 0 R" % x for x in fields))
    doc.xref_set_key(doc.pdf_catalog(), "AcroForm/NeedAppearances", "false")


# ---------------------------------------------------------------- вимірювання
def measure(regs, layer, art, step=0.5):
    """Кліки: скільки влучає у свою область, скільки площі «нічийної».

    Плюс збіг форми: відношення перетину «площа областей ∩ площа
    прямокутників» до їх об'єднання (IoU) — саме це видно оком як «підсвітка
    повторює фігуру».
    """
    nx = int(art.width / step) + 1
    ny = int(art.height / step) + 1
    #  власник точки — ОСТАННЯ область, у контур якої точка потрапляє
    owner = [-1] * (nx * ny)
    for k, reg in enumerate(regs):
        m = HT.scan_mask(reg, art.x0, art.y0, nx, ny, step)
        for i in range(nx * ny):
            if m[i]:
                owner[i] = k
    #  ловець — ОСТАННІЙ у /Annots прямокутник, що містить точку
    catch = [-1] * (nx * ny)
    for rc, k in layer:
        ix0 = max(0, int(math.ceil((rc.x0 - art.x0) / step - 0.5)))
        ix1 = min(nx - 1, int(math.floor((rc.x1 - art.x0) / step - 0.5)))
        iy0 = max(0, int(math.ceil((rc.y0 - art.y0) / step - 0.5)))
        iy1 = min(ny - 1, int(math.floor((rc.y1 - art.y0) / step - 0.5)))
        for iy in range(iy0, iy1 + 1):
            row = iy * nx
            for ix in range(ix0, ix1 + 1):
                catch[row + ix] = k
    tot = nx * ny
    own = hit = dead = cov = 0
    for i in range(tot):
        o, c = owner[i], catch[i]
        if o >= 0:
            own += 1
            if c >= 0:
                cov += 1
            if c == o:
                hit += 1
        elif c >= 0:
            dead += 1
    return {"in_region": own, "hit": hit, "dead": dead,
            "hit_pct": 100.0 * hit / max(1, own),
            "dead_pct": 100.0 * dead / max(1, tot),
            "iou": 100.0 * cov / max(1, own + dead)}


def one_page(src_path):
    doc = fitz.open()
    s = fitz.open(src_path)
    doc.insert_pdf(s, from_page=PNO, to_page=PNO, annots=False)
    s.close()
    HT.PAGE_H = doc[0].rect.height
    return doc


def page_bytes(src_path, builder, off_path, on_path):
    """Одна сторінка + свої віджети й свої трафарети -> байтів на диску.

    Саме це «ціна одного малюнка»: у спільному тестовому файлі трафарети
    шеряться між сторінками, тож із нього приріст не видно.

    Зберігаємо два файли: як поїде замовнику (нічого не зафарбовано) і той
    самий із позначеними полями — для перевірки шва. Позначати треба **до**
    збереження: `garbage=3` перенумеровує xref, і в записаному файлі номери
    об'єктів уже інші.
    """
    doc = one_page(src_path)
    res = builder(doc)
    doc.save(off_path, deflate=True, garbage=3)
    doc.close()
    n = os.path.getsize(off_path)
    mark_all(off_path, on_path)
    return n, res


def mark_all(off_path, on_path):
    """Копія файлу, у якій зафарбовано все. Номери об'єктів беремо з ГОТОВОГО
    файлу: `garbage=3` перенумеровує xref і в самому документі теж."""
    d = fitz.open(off_path)
    ann = re.findall(r"(\d+) 0 R", d.xref_get_key(d[0].xref, "Annots")[1])
    fld = re.findall(r"(\d+) 0 R",
                     d.xref_get_key(d.pdf_catalog(), "AcroForm/Fields")[1])
    for x in ann:
        d.xref_set_key(int(x), "AS", "/Yes")
    for x in fld:
        d.xref_set_key(int(x), "V", "/Yes")
    d.save(on_path, deflate=True, garbage=3)
    d.close()


def timing(path, dpi=150, runs=3):
    """Час «відкрити + відрендерити сторінку» — те, що відчує переглядач."""
    best = 1e9
    for _ in range(runs):
        t = time.perf_counter()
        d = fitz.open(path)
        d[0].get_pixmap(dpi=dpi)
        d.close()
        best = min(best, time.perf_counter() - t)
    return best


def walk_time(path):
    """Час обходу форми — не рендер, а побудова переліку полів.

    Саме тут ховається реальна ціна тисяч віджетів. Рендер лінійний і швидкий
    (потоки вигляду крихітні), а от обхід у PyMuPDF **квадратичний**: кожен
    нащадок змушує наново прочитати `/Kids` батька, а в батька їх тисячі.
    Чи так поводиться Acrobat — файлом не перевіриш, тільки руками; але число
    показує, де саме ризик.
    """
    d = fitz.open(path)
    t = time.perf_counter()
    n = len(list(d[0].widgets()))
    dt = time.perf_counter() - t
    d.close()
    return dt, n


def render_marked(path, clip, dpi, out):
    d = fitz.open(path)
    d[0].get_pixmap(dpi=dpi, clip=clip).save(out)
    d.close()


def _px(path):
    p = fitz.Pixmap(path)
    return p.samples, p.n, p.width, p.height


def _painted(s, n, base, w, h, thr=6):
    """Пікселі, які фарба змінила проти чистої сторінки."""
    m = bytearray(w * h)
    for i in range(w * h):
        o = i * n
        if (abs(s[o] - base[o]) > thr or abs(s[o + 1] - base[o + 1]) > thr
                or abs(s[o + 2] - base[o + 2]) > thr):
            m[i] = 1
    return m


def _erode(m, w, h, r=2):
    return HT.morph(m, w, h, r, False)


def seam_metric(grid_png, ctrl_png, base_png, thr=6, map_out=None):
    """Шов — це відхилення ВСЕРЕДИНІ залитої площі, а не на її краю.

    Прямий диф «сітка проти одного прямокутника» показує ще й те, що по
    периметру області частина трафарету не залита (прикордонні клітинки з
    покриттям < 20 % відкинуто). Це не шов. Тому міряємо тільки там, де
    зафарбовано **обидва** рендери, з відступом 2 px від межі: шов — тонка
    світла лінія всередині суцільної заливки, вона відступ переживає.
    """
    sg, n, w, h = _px(grid_png)
    sc, _, _, _ = _px(ctrl_png)
    sb, _, _, _ = _px(base_png)
    both = bytearray(w * h)
    mg = _painted(sg, n, sb, w, h, thr)
    mc = _painted(sc, n, sb, w, h, thr)
    for i in range(w * h):
        both[i] = 1 if (mg[i] and mc[i]) else 0
    core = _erode(both, w, h, 2)
    mx = tot = bad = cnt = 0
    hot = bytearray(w * h)
    for i in range(w * h):
        if not core[i]:
            continue
        o = i * n
        d = max(abs(sg[o] - sc[o]), abs(sg[o + 1] - sc[o + 1]),
                abs(sg[o + 2] - sc[o + 2]))
        cnt += 1
        tot += d
        if d > mx:
            mx = d
        if d > 8:
            bad += 1
            hot[i] = 1
    if map_out:
        buf = bytearray(w * h * 3)
        for i in range(w * h):
            o = i * 3
            if hot[i]:
                buf[o], buf[o + 1], buf[o + 2] = 230, 20, 20
            elif core[i]:
                buf[o] = buf[o + 1] = buf[o + 2] = 215
            else:
                v = sg[i * n]
                buf[o] = buf[o + 1] = buf[o + 2] = 245 if v > 200 else 160
        fitz.Pixmap(fitz.csRGB, w, h, bytes(buf), False).save(map_out)
    #  скільки трафарету взагалі залито: клітинки з покриттям < COVER
    #  відкидаються, тож по периметру заливка обрізається сходинками
    fill = 100.0 * sum(mg) / max(1, sum(mc))
    return mx, tot / float(max(1, cnt)), bad, cnt, fill


def highlight_sim(path, layer, clip, dpi, out, tint=(120, 160, 235)):
    """Як побачить дитина ДО першого кліку: переглядач тінтує прямокутники полів.

    Власний рендер PyMuPDF цього не малює, тож підсвітку домальовуємо самі —
    інакше головного (форми підсвітки) на рендері просто не видно.
    """
    d = fitz.open(path)
    px = d[0].get_pixmap(dpi=dpi, clip=clip)
    d.close()
    sc = clip.width / px.width
    buf = bytearray(px.samples)
    n = px.n
    #  дискретизуємо прямокутники в піксельну маску — інакше O(px × віджетів)
    m = bytearray(px.width * px.height)
    for rc, _ in layer:
        x0 = max(0, int((rc.x0 - clip.x0) / sc + 0.5))
        x1 = min(px.width, int((rc.x1 - clip.x0) / sc + 0.5))
        y0 = max(0, int((rc.y0 - clip.y0) / sc + 0.5))
        y1 = min(px.height, int((rc.y1 - clip.y0) / sc + 0.5))
        for iy in range(y0, y1):
            row = iy * px.width
            for ix in range(x0, x1):
                m[row + ix] = 1
    for i in range(px.width * px.height):
        if m[i]:
            o = i * n
            for q in range(3):
                buf[o + q] = (buf[o + q] * 62 + tint[q] * 38) // 100
    fitz.Pixmap(fitz.csRGB, px.width, px.height, bytes(buf), False).save(out)


# ---------------------------------------------------------------- заголовок
def title(page, head, body, fontname):
    """Шапку уроку заміняємо підписом варіанта. Червона лінійка на y = 85.5
    лишається, тож у смугу вміщається рівно 4 рядки — більше не пишемо."""
    page.draw_rect(fitz.Rect(18, 12, 578, 82), color=None, fill=(1, 1, 1))
    page.insert_text((28, 36), head, fontname=fontname, fontsize=13,
                     color=(0.85, 0.15, 0.2))
    lines = HT.wrap(body, 124)
    assert len(lines) <= 4, "підпис не вміщається в смугу: %d рядків" % len(lines)
    y = 51
    for chunk in lines:
        page.insert_text((28, y), chunk, fontname=fontname, fontsize=7.6,
                         color=(0.15, 0.15, 0.15))
        y += 9.5


# ---------------------------------------------------------------- головне
def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("--json", default=JSON_PATH)
    ap.add_argument("--src", default=SRC)
    ap.add_argument("--out", default=OUT)
    ap.add_argument("--sizes", default=",".join("%g" % s for s in SIZES))
    ap.add_argument("--render", action="store_true")
    ap.add_argument("--seam-dpi", default="200,400")
    ap.add_argument("--study", default="", help="розмір клітинки для діагностики шва")
    ap.add_argument("--noshare", default="", help="розміри для заміру ціни шерингу")
    ap.add_argument("--walk", action="store_true",
                    help="міряти ще й обхід форми (на 1 pt це ~2 хв)")
    a = ap.parse_args(argv)

    sizes = [float(s) for s in a.sizes.split(",") if s.strip()]
    with open(a.json, encoding="utf-8") as fh:
        j = json.load(fh)
    regs = sorted(j["regions"],
                  key=lambda r: (HT.bbox(r).get_area(), HT.region_area(r)),
                  reverse=True)
    art = HT.bbox(regs[0])
    for r in regs:
        art |= HT.bbox(r)
    poly = sum(HT.region_area(r) for r in regs)

    #  растр сторінки — один раз на весь запуск
    scale = 72.0 / HT.RASTER_DPI
    clip = fitz.Rect(art.x0 - 4, art.y0 - 4, art.x1 + 4, art.y1 + 4)
    s = fitz.open(a.src)
    pix = s[PNO].get_pixmap(dpi=HT.RASTER_DPI, clip=clip, colorspace=fitz.csGRAY)
    s.close()
    paper = bytearray(1 if v >= HT.INK else 0 for v in pix.samples)
    pw, ph = pix.width, pix.height
    bg = HT.page_background(paper, pw, ph)
    ink_ok = bytearray(paper[i] and not bg[i] for i in range(pw * ph))
    ox, oy = clip.x0, clip.y0

    print("малюнок: [%.0f %.0f %.0f %.0f] = %.0f pt², областей %d, "
          "сума площ контурів %.0f pt²\n"
          % (art.x0, art.y0, art.x1, art.y1, art.get_area(), len(regs), poly))

    #  ---- крок 1: по одному однопаге́вому файлу на розмір -> чесна вага й час
    d = one_page(a.src)
    base_path = os.path.join(OUTDIR, "_hl2_base.pdf")
    d.save(base_path, deflate=True, garbage=3)
    d.close()
    nbase = os.path.getsize(base_path)
    t_base = timing(base_path)
    print("порожня сторінка 16: %.1f КБ, відкрити+рендер %.3f с\n"
          % (nbase / 1024.0, t_base))

    rows = []
    seam_dpis = [int(x) for x in a.seam_dpi.split(",")]
    ctrl_png = {}
    base_png = {}
    for dpi in seam_dpis:
        fn = os.path.join(OUTDIR, "hl2_base_%d.png" % dpi)
        render_marked(base_path, art, dpi, fn)
        base_png[dpi] = fn
    for kind, step in [("grid", s) for s in sizes] + [("rect", None)]:
        tag = ("g%g" % step) if kind == "grid" else "ctrl"

        def builder(doc, kind=kind, step=step):
            st = build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale)
            if kind == "grid":
                ann, fld, lay, per = add_grid(doc, regs, st, step, "gp%g" % step)
            else:
                ann, fld, lay, per = add_rects(doc, regs, st, "ctrl")
            attach(doc, doc[0], ann, fld)
            acroform(doc, fld)
            return ann, fld, lay, per, st

        #  однопагевий файл на розмір — саме з нього взята «вага на малюнок»
        off_path = os.path.join(OUTDIR, "hl2_page_%s.pdf" % tag)
        on_path = os.path.join(OUTDIR, "_hl2_%s_marked.pdf" % tag)
        t0 = time.perf_counter()
        nfull, (ann, fld, lay, per, st) = page_bytes(a.src, builder,
                                                     off_path, on_path)
        build_s = time.perf_counter() - t0

        m = measure(regs, lay, art)
        tm = timing(off_path)
        wt = walk_time(off_path)[0] if a.walk else None
        png = {}
        for dpi in seam_dpis:
            fn = os.path.join(OUTDIR, "hl2_%s_on_%d.png" % (tag, dpi))
            render_marked(on_path, art, dpi, fn)
            png[dpi] = fn
        if kind == "rect":
            ctrl_png = png
        hl = os.path.join(OUTDIR, "hl2_%s_highlight.png" % tag)
        highlight_sim(off_path, lay, art, 200, hl)

        rows.append(dict(kind=kind, step=step, tag=tag, n=len(ann),
                         kb=(nfull - nbase) / 1024.0, base=nbase / 1024.0,
                         full=nfull / 1024.0, m=m, t=tm, build=build_s,
                         png=png, hl=hl, per=per, pdf=on_path, off=off_path,
                         walk=wt))
        print("%-5s віджетів %5d | +%7.1f КБ | кліки %5.1f %% | нічийне %4.1f %% | "
              "IoU %4.1f %% | відкрити+рендер %.3f с | збірка %.1f с"
              % (tag, len(ann), rows[-1]["kb"], m["hit_pct"], m["dead_pct"],
                 m["iou"], tm, build_s)
              + ("" if wt is None else " | обхід форми %.1f с" % wt))

    #  ---- крок 2: шов = відхилення ВСЕРЕДИНІ заливки проти контрольної
    print("\nшов (диф усередині залитої площі, відступ 2 px від краю; "
          "контроль — той самий трафарет одним прямокутником):")
    for r in rows:
        if r["kind"] != "grid":
            continue
        r["seam"] = {}
        for dpi in seam_dpis:
            mp = os.path.join(OUTDIR, "hl2_%s_seammap_%d.png" % (r["tag"], dpi))
            d = seam_metric(r["png"][dpi], ctrl_png[dpi], base_png[dpi],
                            map_out=mp)
            r["seam"][dpi] = d
            print("   %-5s %4d dpi: max Δ %3d, сер. Δ %.3f, пікселів Δ>8: %d з %d"
                  "  | залито %.1f %% трафарету"
                  % (r["tag"], dpi, d[0], d[1], d[2], d[3], d[4]))

    #  ---- крок 2b: діагностика — звідки шов брався
    seam_note = {}
    if a.study:
        st_size = float(a.study)
        print("\nдіагностика на клітинці %g pt — що саме прибирає шов:" % st_size)
        for name, ov, tr in (("напівпрозора, без перекриття (як зараз, Б)", 0.0, True),
                             ("напівпрозора, перекриття 0.25", OVERLAP, True),
                             ("непрозора, без перекриття", 0.0, False),
                             ("непрозора, перекриття 0.25 (нове)", OVERLAP, False)):
            def bld(doc, ov=ov, tr=tr, st_size=st_size):
                st = build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale, tr)
                ann, fld, lay, per = add_grid(doc, regs, st, st_size, "sty",
                                              overlap=ov, translucent=tr)
                attach(doc, doc[0], ann, fld)
                acroform(doc, fld)
                return ann, fld, lay, per, st
            o = os.path.join(OUTDIR, "_hl2_study_off.pdf")
            n_ = os.path.join(OUTDIR, "_hl2_study_on.pdf")
            page_bytes(a.src, bld, o, n_)
            #  контроль для напівпрозорого — свій (інакше диф зловить прозорість)
            def bldc(doc, tr=tr):
                st = build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale, tr)
                ann, fld, lay, per = add_rects(doc, regs, st, "cst")
                if tr:
                    for i, reg in enumerate(regs, 1):
                        pass
                attach(doc, doc[0], ann, fld)
                acroform(doc, fld)
                return ann, fld, lay, per, st
            oc = os.path.join(OUTDIR, "_hl2_studyc_off.pdf")
            nc = os.path.join(OUTDIR, "_hl2_studyc_on.pdf")
            page_bytes(a.src, bldc, oc, nc)
            for dpi in seam_dpis:
                g = os.path.join(OUTDIR, "hl2_study_%d.png" % dpi)
                c = os.path.join(OUTDIR, "hl2_studyc_%d.png" % dpi)
                render_marked(n_, art, dpi, g)
                render_marked(nc, art, dpi, c)
                d = seam_metric(g, c, base_png[dpi])
                print("   %-42s %4d dpi: max Δ %3d, сер. Δ %.3f, Δ>8: %d з %d"
                      % (name, dpi, d[0], d[1], d[2], d[3]))
                seam_note.setdefault(name, {})[dpi] = d
                if name.startswith("непрозора, перекриття"):
                    os.replace(g, os.path.join(OUTDIR, "hl2_fixed_%d.png" % dpi))
                elif name.startswith("напівпрозора, без"):
                    os.replace(g, os.path.join(OUTDIR, "hl2_seam_old_%d.png" % dpi))

    #  ---- крок 2c: скільки дає шеринг XObject
    if a.noshare:
        print("\nшеринг XObject — той самий розмір клітинки з власним трафаретом "
              "у кожної клітинки:")
        for sz in [float(x) for x in a.noshare.split(",")]:
            def bldn(doc, sz=sz):
                st = build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale)
                ann, fld, lay, per = add_grid_noshare(doc, regs, st, sz, "nsh")
                attach(doc, doc[0], ann, fld)
                acroform(doc, fld)
                return ann, fld, lay, per, st
            o = os.path.join(OUTDIR, "_hl2_noshare_off.pdf")
            n_ = os.path.join(OUTDIR, "_hl2_noshare_on.pdf")
            nn, (ann, _, _, _, _) = page_bytes(a.src, bldn, o, n_)
            same = [r for r in rows if r["kind"] == "grid" and r["step"] == sz]
            kb = (nn - nbase) / 1024.0
            ref = same[0]["kb"] if same else 0
            print("   %g pt: без шерингу +%.0f КБ (%.0f Б/клітинку), "
                  "зі спільним трафаретом +%.0f КБ (%.0f Б/клітинку) — у %.2f раза легше"
                  % (sz, kb, kb * 1024 / max(1, len(ann)), ref,
                     ref * 1024 / max(1, len(ann)), kb / max(1e-6, ref)))

    #  ---- крок 3: спільний файл із підписами
    doc = fitz.open()
    for _ in range(len(rows)):
        s = fitz.open(a.src)
        doc.insert_pdf(s, from_page=PNO, to_page=PNO, annots=False)
        s.close()
    HT.PAGE_H = doc[0].rect.height
    cs = [doc.xref_get_key(p.xref, "Contents") for p in doc]
    assert len(set(map(str, cs))) == len(rows), "сторінки поділили один потік"
    doc[0].insert_font(fontfile=FONT, fontname="ArB")
    for p in doc[1:]:
        p.insert_font(fontfile=FONT, fontname="ArB")

    st = build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale)   # спільні
    allf = []
    for i, r in enumerate(rows):
        if r["kind"] == "grid":
            ann, fld, lay, per = add_grid(doc, regs, st, r["step"], "g%g" % r["step"])
            head = ("КЛІТИНКА %g pt — %s віджетів, +%s КБ на малюнок"
                    % (r["step"], fmt(r["n"]), fmt(round(r["kb"]))))
            sm = r["seam"][max(seam_dpis)]
            body = ("Непрозора фарба + растровий трафарет: чорні штрихи всередині "
                    "області лишаються видимі. Клітинки перекриваються на чверть "
                    "кроку — шва немає (диф проти контрольної стор.: %d пікс. із "
                    "%d при %d dpi). Клік у будь-яку клітинку фарбує ВСЮ область. "
                    "Кліків у ціль %.1f %%, «нічийної» площі %.1f %%, збіг "
                    "прямокутників із фігурою %.0f %%, залито %.0f %% фарби, "
                    "відкрити+рендер %.2f с."
                    % (sm[2], sm[3], max(seam_dpis), r["m"]["hit_pct"],
                       r["m"]["dead_pct"], r["m"]["iou"], sm[4], r["t"]))
        else:
            ann, fld, lay, per = add_rects(doc, regs, st, "ctrl")
            head = ("КОНТРОЛЬ, ВАРІАНТ В — %d віджетів, +%s КБ на малюнок"
                    % (r["n"], fmt(round(r["kb"]))))
            body = ("Один прямокутник на область, той самий трафарет і та сама "
                    "непрозора фарба. Фарба лягає повністю (100 %%) — різниця "
                    "тільки в ПІДСВІТЦІ: переглядач тінтує коробку навколо фігури, "
                    "а не саму фігуру. Кліків у ціль %.1f %%, «нічийної» площі "
                    "%.1f %%, збіг прямокутників із фігурою %.0f %%, "
                    "відкрити+рендер %.2f с."
                    % (r["m"]["hit_pct"], r["m"]["dead_pct"], r["m"]["iou"], r["t"]))
        attach(doc, doc[i], ann, fld)
        title(doc[i], head, body, "ArB")
        allf += fld
    acroform(doc, allf)
    os.makedirs(os.path.dirname(a.out) or ".", exist_ok=True)
    doc.subset_fonts()
    doc.save(a.out, deflate=True, garbage=3)
    doc.close()
    print("\n%s: %.0f КБ (трафарети спільні на всі сторінки)"
          % (a.out, os.path.getsize(a.out) / 1024))

    # ---- таблиця
    dpi_max = max(seam_dpis)
    print("\n%-8s %8s %10s %8s %8s %7s %7s %9s %8s"
          % ("клітинка", "віджетів", "+КБ/малюн.", "кліки", "нічийне", "форма",
             "залито", "шов, px", "с/стор."))
    for r in rows:
        sm = r.get("seam", {}).get(dpi_max)
        print("%-8s %8s %10.1f %7.1f%% %7.1f%% %6.1f%% %6.1f%% %9s %8.3f"
              % (("%g pt" % r["step"]) if r["kind"] == "grid" else "В (11)",
                 fmt(r["n"]), r["kb"], r["m"]["hit_pct"], r["m"]["dead_pct"],
                 r["m"]["iou"], sm[4] if sm else 100.0,
                 ("%d/%d" % (sm[2], sm[3])) if sm else "—", r["t"])
              + ("" if r["walk"] is None else "  обхід %8.1f с" % r["walk"]))
    print("\nбайтів на віджет (за вирахуванням трафаретів):")
    ctrl = [r for r in rows if r["kind"] == "rect"][0]
    for r in rows:
        if r["kind"] != "grid":
            continue
        extra = (r["kb"] - ctrl["kb"]) * 1024
        print("   %-6s %6.0f Б/клітинку (понад ціну 11 трафаретів = %.0f КБ)"
              % ("%g pt" % r["step"], extra / max(1, r["n"] - 11), ctrl["kb"]))
    return 0


if __name__ == "__main__":
    sys.exit(main())
