#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Адаптивне розбиття області на прямокутники замість рівномірної сітки.

Навіщо. У варіанті «сітка 2 pt» (`verify/highlight_grid.py`) підсвітка повторює
фігуру з точністю до 2 pt, але коштує 1 615 віджетів: усередині області стоїть
кілька сотень однакових квадратиків, які нічого не уточнюють — там і так усе
всередині. Точність потрібна лише **вздовж контуру**.

Що робимо. Класичне **прямокутне розбиття бінарної маски**, без жодної
оптимізації «максимальних вписаних прямокутників»:

1. область растеризується тією самою сіткою `step` pt, що й раніше
   (`highlight_grid.cells_of`, поріг покриття 0.20) — множина клітинок
   лишається **побітово тією самою**;
2. у кожному рядку суміжні клітинки зливаються в горизонтальний пробіг;
3. пробіги сусідніх рядків із **однаковими межами** зливаються вертикально.

Результат — набір прямокутників, що не перетинаються і в об'єднанні дають рівно
ту саму множину клітинок. Дві умови коректності перевіряються **числом** на
кожній області (`check_partition`):

* кожна клітинка кожного прямокутника належить масці — тобто прямокутник лежить
  усередині області **цілком**, а не лише кутами. Для ввігнутих областей
  (сорочка з вирізом для руки, поділ із розрізом) перевірка по кутах хибна;
* об'єднання прямокутників = множина клітинок рівномірної сітки, клітинка в
  клітинку. Будь-яка втрата площі — помилка злиття, а не «оптимізація».

Вигляд не змінюється: одне поле на область, прямокутники — його `/Kids`, кожен
малює свій шматок **спільного** трафарету області (`/ImageMask`), обрізаний
власним `/BBox`, фарба непрозора. Великий прямокутник просто малює більший
шматок того самого трафарету.

Виміряно на стор. 16 (11 областей). Перекриття міняє площу об'єднання, тож
порівнювати можна лише однакові режими — звідси 2×2:

| спосіб | віджетів | +КБ | кліки | нічийне | форма | залито | шов 400 dpi | обхід |
|---|---|---|---|---|---|---|---|---|
| адаптивне 2 pt          |   138 |  +67 | 98.5 % | 2.7 % | 88.7 % | 97.5 % | 0/154 745 | 0.09 с |
| адаптивне 3 pt          |    93 |  +46 | 97.8 % | 4.1 % | 84.1 % | 97.6 % | 0/154 660 | 0.06 с |
| адаптивне 2 pt + перекр.|   138 |  +69 | 98.9 % | 3.8 % | 85.6 % | 99.0 % | 5/157 071 | 0.09 с |
| рівномірне 2 pt (v02)   | 1 615 | +702 | 98.9 % | 3.8 % | 85.6 % | 99.1 % | 13/157 067 | 8.9 с |
| рівномірне 2 pt без пер.| 1 615 | +685 | 98.5 % | 2.7 % | 88.7 % | 97.5 % | 3/154 742 | 9.4 с |
| В (11 прямокутників)    |    11 |   +8 | 96.7 % | 16.9 % | 57.8 % | 100 % | — | 0.00 с |

Головне: **рядок «адаптивне» збігається з рядком «рівномірне» того самого
режиму до десятої частки відсотка**, а симуляція підсвітки збігається
**побітово** (0 різних пікселів із 192 818). Тобто 11.7× менше віджетів і
10.5× менша вага куплені без жодної втрати якості; обхід форми з 8.9 с падає
до 0.09 с. Заливка адаптивного при цьому на волосину чистіша за рівномірну
(0 проти 3 пікселів шва при 400 dpi без перекриття, 5 проти 13 — з
перекриттям): менше внутрішніх стиків — менше місць, де клип `/BBox`
антиаліасить фарбу двічі.

Запуск:
    .venv/bin/python verify/highlight_adaptive.py
    .venv/bin/python verify/highlight_adaptive.py --sizes 2,3 --no-walk
"""
import argparse
import json
import os
import sys
import time

import fitz

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

OUT = "verify/highlight_test_v03.pdf"
OUTDIR = HG.OUTDIR


# ---------------------------------------------------------------- розбиття
def cell_index(cells, step):
    """Клітинки -> множина цілих індексів (ix, iy). Сітка глобальна, кратна step."""
    s = set()
    for c in cells:
        s.add((int(round(c.x0 / step)), int(round(c.y0 / step))))
    assert len(s) == len(cells), "клітинки повторюються"
    return s


def partition(idx):
    """Прямокутне розбиття множини клітинок: рядкові пробіги + вертикальне злиття.

    Повертає перелік `(x0, y0, x1, y1)` **включно** по обох кінцях, в індексах
    клітинок. Прямокутники не перетинаються за побудовою: у межах рядка пробіги
    розділені дірками, а вертикальне злиття лише подовжує пробіг із тими самими
    межами по x.
    """
    rows = {}
    for ix, iy in idx:
        rows.setdefault(iy, []).append(ix)
    runs = {}
    for iy, xs in rows.items():
        cur = []
        for ix in sorted(xs):
            if cur and cur[-1][1] == ix - 1:
                cur[-1][1] = ix
            else:
                cur.append([ix, ix])
        runs[iy] = [tuple(r) for r in cur]

    out = []
    live = {}                                   # (x0, x1) -> y-початок, y-кінець
    for iy in sorted(runs):
        nxt = {}
        for key in runs[iy]:
            prev = live.get(key)
            nxt[key] = (prev[0], iy) if (prev and prev[1] == iy - 1) else (iy, iy)
        for key, (y0, y1) in live.items():      # усе, що не продовжилося
            if key not in nxt or nxt[key][0] != y0:
                out.append((key[0], y0, key[1], y1))
        live = nxt
    for key, (y0, y1) in live.items():
        out.append((key[0], y0, key[1], y1))
    return out


def check_partition(parts, idx, name=""):
    """Дві умови коректності — обидві числом, а не «на око».

    1) прямокутник ЦІЛКОМ усередині маски: перевіряються ВСІ його клітинки;
    2) площа покриття не впала: об'єднання = маска, клітинка в клітинку.
    """
    seen = set()
    for x0, y0, x1, y1 in parts:
        for iy in range(y0, y1 + 1):
            for ix in range(x0, x1 + 1):
                assert (ix, iy) in idx, (
                    "%s: прямокутник (%d %d %d %d) вийшов за область у (%d, %d)"
                    % (name, x0, y0, x1, y1, ix, iy))
                assert (ix, iy) not in seen, (
                    "%s: клітинка (%d, %d) накрита двічі" % (name, ix, iy))
                seen.add((ix, iy))
    assert seen == idx, ("%s: покриття впало — %d клітинок із %d"
                         % (name, len(seen), len(idx)))
    return len(seen)


def corner_trap(idx):
    """Скільки прямокутників пройшли б перевірку ПО КУТАХ, але вилазять з області.

    Наші області ввігнуті (сорочка з вирізом для руки, поділ із розрізом,
    «сандалі» — це взагалі два окремі полігони), тож перевірка «всі чотири кути
    всередині» неправильна. Число нижче — прямий доказ, що це не теорія:
    кожен такий прямокутник зафарбував би сусідню деталь чужим кольором.
    """
    xs = sorted({p[0] for p in idx})
    ys = sorted({p[1] for p in idx})
    bad, ex = 0, None
    for y0 in range(ys[0], ys[-1] + 1):
        for y1 in range(y0, ys[-1] + 1):
            for x0 in range(xs[0], xs[-1] + 1):
                for x1 in range(x0, xs[-1] + 1):
                    if not all((x, y) in idx for x in (x0, x1) for y in (y0, y1)):
                        continue
                    if not all((x, y) in idx for y in range(y0, y1 + 1)
                               for x in range(x0, x1 + 1)):
                        bad += 1
                        ex = ex or (x0, y0, x1, y1)
    return bad, ex


def rects_of(reg, step, thr=HG.COVER):
    """Прямокутники адаптивного розбиття області + діагностика."""
    cells = HG.cells_of(reg, step, thr)
    idx = cell_index(cells, step)
    parts = partition(idx)
    n = check_partition(parts, idx, reg.get("name", ""))
    rc = [fitz.Rect(x0 * step, y0 * step, (x1 + 1) * step, (y1 + 1) * step)
          for x0, y0, x1, y1 in parts]
    return rc, len(cells), n


# ---------------------------------------------------------------- сторінка
def add_adaptive(doc, regs, st, step, prefix, overlap=0.0):
    """Одне поле на область, адаптивні прямокутники — його `/Kids`."""
    annots, fields, layer, per = [], [], [], []
    e = step * overlap / 2.0
    offs = {}
    for i, reg in enumerate(regs, 1):
        im, res, mrect = st[i - 1][:3]
        rc, ncell, _ = rects_of(reg, step)
        if not rc:
            rc = [HT.bbox(reg)]
        kids = []
        for c in rc:
            box = fitz.Rect(c.x0 - e, c.y0 - e, c.x1 + e, c.y1 + e)
            on = HG.form_xobj_res(doc, box.width, box.height, res,
                                  HG.paint_op(reg, mrect, box))
            key = (round(box.width, 3), round(box.height, 3))
            if key not in offs:                 # порожній `Off` — спільний на розмір
                offs[key] = HT.form_xobj(doc, box.width, box.height, "", "")
            kids.append((HT.widget_obj(doc, box, on, offs[key]), 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), ncell))
    return annots, fields, layer, per


# ---------------------------------------------------------------- картинки
def sim(path, layer, clip, dpi, out, tint=(120, 160, 235), edges=False):
    """Симуляція підсвітки: переглядач тінтує прямокутник кожного віджета.

    `edges=True` домальовує межі прямокутників — саме цим адаптивне розбиття
    відрізняється від рівномірного (тінт-об'єднання в них однакове).
    """
    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, W, H = px.n, px.width, px.height
    m = bytearray(W * H)
    ed = bytearray(W * H)
    for rc, _ in layer:
        x0 = max(0, min(W, int((rc.x0 - clip.x0) / sc + 0.5)))
        x1 = max(0, min(W, int((rc.x1 - clip.x0) / sc + 0.5)))
        y0 = max(0, min(H, int((rc.y0 - clip.y0) / sc + 0.5)))
        y1 = max(0, min(H, int((rc.y1 - clip.y0) / sc + 0.5)))
        for iy in range(y0, y1):
            row = iy * W
            for ix in range(x0, x1):
                m[row + ix] = 1
        if edges:
            for ix in range(x0, x1):
                if y0 < H:
                    ed[y0 * W + ix] = 1
                if y1 - 1 >= 0:
                    ed[(y1 - 1) * W + ix] = 1
            for iy in range(y0, y1):
                if x0 < W:
                    ed[iy * W + x0] = 1
                if x1 - 1 >= 0:
                    ed[iy * W + x1 - 1] = 1
    for i in range(W * H):
        o = i * n
        if ed[i]:
            buf[o], buf[o + 1], buf[o + 2] = 40, 70, 150
        elif m[i]:
            for q in range(3):
                buf[o + q] = (buf[o + q] * 62 + tint[q] * 38) // 100
    fitz.Pixmap(fitz.csRGB, W, H, bytes(buf), False).save(out)
    return out


def png_diff(a, b):
    """Скільки пікселів різняться — щоб «невідрізненно» було числом."""
    pa, pb = fitz.Pixmap(a), fitz.Pixmap(b)
    if (pa.width, pa.height, pa.n) != (pb.width, pb.height, pb.n):
        return -1, 0
    sa, sb, n = pa.samples, pb.samples, pa.n
    bad = mx = 0
    for i in range(pa.width * pa.height):
        o = i * n
        d = max(abs(sa[o] - sb[o]), abs(sa[o + 1] - sb[o + 1]),
                abs(sa[o + 2] - sb[o + 2]))
        if d > 0:
            bad += 1
            mx = max(mx, d)
    return bad, mx


def side_by_side(paths, out, gap=10):
    ps = [fitz.Pixmap(p) for p in paths]
    W = sum(p.width for p in ps) + gap * (len(ps) - 1)
    H = max(p.height for p in ps)
    buf = bytearray(b"\xff" * (W * H * 3))
    ox = 0
    for p in ps:
        s, n = p.samples, p.n
        for iy in range(p.height):
            row = (iy * W + ox) * 3
            src = iy * p.width * n
            for ix in range(p.width):
                o = row + ix * 3
                buf[o] = s[src + ix * n]
                buf[o + 1] = s[src + ix * n + 1]
                buf[o + 2] = s[src + ix * n + 2]
        ox += p.width + gap
    fitz.Pixmap(fitz.csRGB, W, H, bytes(buf), False).save(out)
    return out


# ---------------------------------------------------------------- головне
def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("--json", default=HG.JSON_PATH)
    ap.add_argument("--src", default=HG.SRC)
    ap.add_argument("--out", default=OUT)
    ap.add_argument("--sizes", default="2,3", help="розміри клітинки адаптивного")
    ap.add_argument("--base", default="2", help="розмір рівномірної бази")
    ap.add_argument("--seam-dpi", default="200,400")
    ap.add_argument("--no-walk", action="store_true")
    ap.add_argument("--quick", action="store_true",
                    help="не міряти 2×2 «з перекриттям / без» (швидше на ~20 с)")
    ap.add_argument("--corner-trap", action="store_true",
                    help="показати, скільки прямокутників пройшли б перевірку "
                         "по кутах, але вилізли б з області (повний перебір)")
    a = ap.parse_args(argv)

    sizes = [float(s) for s in a.sizes.split(",") if s.strip()]
    base = float(a.base)
    seam_dpis = [int(x) for x in a.seam_dpi.split(",")]

    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)

    #  растр сторінки — один раз на весь запуск (джерело трафаретів)
    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[HG.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\n"
          % (art.x0, art.y0, art.x1, art.y1, art.get_area(), len(regs)))

    #  ---- контроль коректності розбиття, окремо й голосно
    print("розбиття — прямокутників проти клітинок (перевірено: жодна клітинка "
          "не втрачена, жоден прямокутник не вийшов за область):")
    part_stat = {}
    for step in sizes:
        tot_r = tot_c = 0
        lines = []
        big = one = multi = 0
        mx = None
        for reg in regs:
            rc, ncell, ncov = rects_of(reg, step)
            tot_r += len(rc)
            tot_c += ncell
            for c in rc:
                nx, ny = int(round(c.width / step)), int(round(c.height / step))
                if nx * ny == 1:
                    one += 1
                if ny > 1:
                    multi += 1
                if nx * ny >= 8:
                    big += nx * ny
                if mx is None or nx * ny > mx[0] * mx[1]:
                    mx = (nx, ny)
            lines.append("   %-22s %5d -> %4d  (%.1f× менше)"
                         % (reg["name"], ncell, len(rc),
                            ncell / float(max(1, len(rc)))))
        part_stat[step] = (tot_c, tot_r)
        print("  клітинка %g pt:" % step)
        for ln in lines:
            print(ln)
        print("   %-22s %5d -> %4d  (%.1f× менше)"
              % ("РАЗОМ", tot_c, tot_r, tot_c / float(max(1, tot_r))))
        print("   найбільший прямокутник %d×%d клітинок; у прямокутниках "
              "від 8 клітинок лежить %.0f %% площі; одноклітинкових %d; "
              "склеєних більш ніж по одному рядку %d\n"
              % (mx[0], mx[1], 100.0 * big / max(1, tot_c), one, multi))

    if a.corner_trap:
        print("перевірка по КУТАХ була б хибною — прямокутників із чотирма "
              "кутами в області, які з неї вилазять (крок %g pt):" % sizes[0])
        tot = 0
        for reg in regs:
            idx = cell_index(HG.cells_of(reg, sizes[0], HG.COVER), sizes[0])
            bad, ex = corner_trap(idx)
            tot += bad
            print("   %-22s %5d%s" % (reg["name"], bad,
                                      "" if ex is None else "   напр. %s" % (ex,)))
        print("   %-22s %5d\n" % ("РАЗОМ", tot))

    #  ---- порожня сторінка = базова вага
    d = HG.one_page(a.src)
    base_path = os.path.join(OUTDIR, "_hl3_base.pdf")
    d.save(base_path, deflate=True, garbage=3)
    d.close()
    nbase = os.path.getsize(base_path)
    print("порожня сторінка 16: %.1f КБ\n" % (nbase / 1024.0))

    base_png = {}
    for dpi in seam_dpis:
        fn = os.path.join(OUTDIR, "hl3_base_%d.png" % dpi)
        HG.render_marked(base_path, art, dpi, fn)
        base_png[dpi] = fn

    #  (вид, крок, перекриття, підпис, чи йде окремою сторінкою у файл)
    variants = [("ctrl", None, 0.0, "В (11 прямокутників)", True)]
    for step in sizes:
        variants.append(("adapt", step, 0.0, "адаптивне %g pt" % step, True))
    variants.append(("grid", base, HG.OVERLAP,
                     "рівномірне %g pt (база v02)" % base, True))
    if not a.quick:
        #  чесне 2×2: перекриття міняє площу об'єднання, тож порівнювати треба
        #  однакові режими. Ці рядки — тільки в таблицю, сторінок не додають.
        for step in sizes:
            variants.append(("adapt_ov", step, HG.OVERLAP,
                             "адаптивне %g pt + перекр." % step, False))
        variants.append(("grid_no", base, 0.0,
                         "рівномірне %g pt без перекр." % base, False))

    rows = []
    ctrl_png = {}
    for kind, step, ov, label, on_page in variants:
        tag = {"ctrl": "ctrl"}.get(kind, "%s%g" % (kind, step or 0))

        def builder(doc, kind=kind, step=step, ov=ov):
            st = HG.build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale)
            if kind == "ctrl":
                ann, fld, lay, per = HG.add_rects(doc, regs, st, "ctrl")
            elif kind.startswith("grid"):
                ann, fld, lay, per = HG.add_grid(doc, regs, st, step,
                                                 "u%g" % step, overlap=ov)
            else:
                ann, fld, lay, per = add_adaptive(doc, regs, st, step,
                                                  "a%g" % step, overlap=ov)
            HG.attach(doc, doc[0], ann, fld)
            HG.acroform(doc, fld)
            return ann, fld, lay, per, st

        off_path = os.path.join(OUTDIR, "hl3_page_%s.pdf" % tag)
        on_path = os.path.join(OUTDIR, "_hl3_%s_marked.pdf" % tag)
        t0 = time.perf_counter()
        nfull, (ann, fld, lay, per, st) = HG.page_bytes(a.src, builder,
                                                        off_path, on_path)
        build_s = time.perf_counter() - t0

        m = HG.measure(regs, lay, art)
        tm = HG.timing(off_path)
        wt = None if a.no_walk else HG.walk_time(off_path)[0]
        png = {}
        for dpi in seam_dpis:
            fn = os.path.join(OUTDIR, "hl3_%s_on_%d.png" % (tag, dpi))
            HG.render_marked(on_path, art, dpi, fn)
            png[dpi] = fn
        if kind == "ctrl":
            ctrl_png = png
        hl = sim(off_path, lay, art, 200,
                 os.path.join(OUTDIR, "hl3_%s_highlight.png" % tag))
        hle = sim(off_path, lay, art, 200,
                  os.path.join(OUTDIR, "hl3_%s_edges.png" % tag), edges=True)

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

    #  ---- шов проти контрольної (той самий трафарет одним прямокутником)
    print("\nшов (диф усередині залитої площі, відступ 2 px від краю):")
    for r in rows:
        if r["kind"] == "ctrl":
            continue
        for dpi in seam_dpis:
            mp = os.path.join(OUTDIR, "hl3_%s_seammap_%d.png" % (r["tag"], dpi))
            dd = HG.seam_metric(r["png"][dpi], ctrl_png[dpi], base_png[dpi],
                                map_out=mp)
            r["seam"][dpi] = dd
            print("   %-28s %4d dpi: max Δ %3d, сер. Δ %.3f, пікселів Δ>8: %d з %d"
                  "  | залито %.1f %% трафарету"
                  % (r["label"], dpi, dd[0], dd[1], dd[2], dd[3], dd[4]))

    #  ---- адаптивне проти рівномірного: заливка й підсвітка, попіксельно.
    #  Порівнюємо з рівномірним ТОГО САМОГО режиму перекриття — інакше диф
    #  показує різницю перекриття, а не різницю розбиття.
    by_kind = {r["kind"]: r for r in rows}
    ref = by_kind.get("grid_no", by_kind["grid"])
    print("\nадаптивне проти рівномірного того самого кроку — попіксельно "
          "(0 = невідрізненно):")
    for r in rows:
        if not r["kind"].startswith("adapt") or r["step"] != base:
            continue
        cmp_ = by_kind["grid"] if r["kind"] == "adapt_ov" else ref
        for dpi in seam_dpis:
            bad, mx = png_diff(r["png"][dpi], cmp_["png"][dpi])
            print("   заливка   %-26s vs %-26s %4d dpi: різних пікселів %d (max Δ %d)"
                  % (r["label"], cmp_["label"], dpi, bad, mx))
        bad, mx = png_diff(r["hl"], cmp_["hl"])
        print("   підсвітка %-26s vs %-26s  200 dpi: різних пікселів %d (max Δ %d)"
              % (r["label"], cmp_["label"], bad, mx))

    sbs = side_by_side([rows[1]["hl"], ref["hl"]],
                       os.path.join(OUTDIR, "hl3_sbs_highlight.png"))
    sbe = side_by_side([rows[1]["hle"], ref["hle"]],
                       os.path.join(OUTDIR, "hl3_sbs_edges.png"))
    sbf = side_by_side([rows[1]["png"][max(seam_dpis)],
                        ref["png"][max(seam_dpis)]],
                       os.path.join(OUTDIR, "hl3_sbs_filled.png"))
    print("\nпоруч: %s (підсвітка), %s (з межами прямокутників), %s (заливка)"
          % (sbs, sbe, sbf))

    #  ---- спільний файл із підписами
    order = [r for r in rows if r["page"] and r["kind"] == "adapt"] \
        + [r for r in rows if r["page"] and r["kind"] == "grid"] \
        + [r for r in rows if r["page"] and r["kind"] == "ctrl"]
    doc = fitz.open()
    for _ in order:
        s = fitz.open(a.src)
        doc.insert_pdf(s, from_page=HG.PNO, to_page=HG.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(order), "сторінки поділили один потік"
    for p in doc:
        p.insert_font(fontfile=HG.FONT, fontname="ArB")

    st = HG.build_stencils(doc, regs, ink_ok, pw, ph, ox, oy, scale)  # спільні
    allf = []
    for i, r in enumerate(order):
        if r["kind"] in ("adapt", "adapt_ov"):
            ann, fld, lay, per = add_adaptive(doc, regs, st, r["step"],
                                              "a%g%s" % (r["step"],
                                                         "o" if r["ov"] else ""),
                                              overlap=r["ov"])
            nc = part_stat[r["step"]][0]
            head = ("АДАПТИВНЕ РОЗБИТТЯ %g pt — %s віджетів, +%s КБ на малюнок"
                    % (r["step"], HG.fmt(r["n"]), HG.fmt(round(r["kb"]))))
            sm = r["seam"][max(seam_dpis)]
            body = ("Усередині області — великі прямокутники, дрібні клітинки %g pt "
                    "лишилися лише вздовж контуру: %s клітинок злито в %s "
                    "прямокутників, покриття те саме до клітинки. Заливка й "
                    "підсвітка збігаються з рівномірною сіткою. Клік у будь-який "
                    "прямокутник фарбує ВСЮ область. Кліків у ціль %.1f %%, "
                    "«нічийної» площі %.1f %%, збіг із фігурою %.0f %%, шов %d пікс. "
                    "із %d при %d dpi."
                    % (r["step"], HG.fmt(nc), HG.fmt(r["n"]), r["m"]["hit_pct"],
                       r["m"]["dead_pct"], r["m"]["iou"], sm[2], sm[3],
                       max(seam_dpis)))
        elif r["kind"].startswith("grid"):
            ann, fld, lay, per = HG.add_grid(doc, regs, st, r["step"],
                                             "u%g" % r["step"], overlap=r["ov"])
            head = ("БАЗА: РІВНОМІРНА СІТКА %g pt — %s віджетів, +%s КБ на малюнок"
                    % (r["step"], HG.fmt(r["n"]), HG.fmt(round(r["kb"]))))
            sm = r["seam"][max(seam_dpis)]
            body = ("Те, з чим порівнюємо: однакові квадратики %g pt по всій площі "
                    "області, зокрема там, де уточнювати нічого. Кліків у ціль "
                    "%.1f %%, «нічийної» площі %.1f %%, збіг із фігурою %.0f %%, "
                    "шов %d пікс. із %d при %d dpi."
                    % (r["step"], r["m"]["hit_pct"], r["m"]["dead_pct"],
                       r["m"]["iou"], sm[2], sm[3], max(seam_dpis)))
        else:
            ann, fld, lay, per = HG.add_rects(doc, regs, st, "ctrl")
            head = ("КОНТРОЛЬ, ВАРІАНТ В — %d віджетів, +%s КБ на малюнок"
                    % (r["n"], HG.fmt(round(r["kb"]))))
            body = ("Один прямокутник на область, той самий трафарет і та сама "
                    "непрозора фарба. Заливка така сама, різниця тільки в "
                    "ПІДСВІТЦІ: переглядач тінтує коробку навколо фігури, а не саму "
                    "фігуру. Кліків у ціль %.1f %%, «нічийної» площі %.1f %%, "
                    "збіг із фігурою %.0f %%."
                    % (r["m"]["hit_pct"], r["m"]["dead_pct"], r["m"]["iou"]))
        HG.attach(doc, doc[i], ann, fld)
        HG.title(doc[i], head, body, "ArB")
        allf += fld
    HG.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 КБ (%d сторінок, трафарети спільні на всі сторінки)"
          % (a.out, os.path.getsize(a.out) / 1024, len(order)))

    #  ---- таблиця
    dmax = max(seam_dpis)
    print("\n%-26s %9s %11s %8s %9s %7s %8s %9s %10s"
          % ("спосіб", "віджетів", "+КБ/малюн.", "кліки", "нічийне", "форма",
             "залито", "шов, px", "обхід, с"))
    for r in rows:
        sm = r["seam"].get(dmax)
        print("%-26s %9s %11.1f %7.1f%% %8.1f%% %6.1f%% %7.1f%% %9s %10s"
              % (r["label"], HG.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 "—",
                 "—" if r["walk"] is None else "%.2f" % r["walk"]))
    return 0


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