#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Тест підсвітки полів форми: одна й та сама картинка — трьома способами.

Навіщо. Замовник відкрив демо клікабельних областей (`verify/Level2_UA_regions_v03.pdf`)
в Acrobat, у браузері й на телефоні: **переглядач малює кольоровий фон на
прямокутнику кожного поля**. З боку файлу прибрано все, що можна (`/MK <<>>`,
`/BS` нульової ширини, `/H /N`, порожній потік `Off`, `NeedAppearances false`) —
тінт малює сам переглядач поверх прямокутника віджета, незалежно від вмісту
потоку вигляду. Керується лише налаштуванням переглядача, файлом — ні.

Отже єдине, чим ми можемо керувати, — **форма й розмір прямокутника**. Звідси
три варіанти, зібрані в один файл, щоб замовник відкрив його раз:

* **А** — як зараз: один віджет на область, прямокутник = bbox контуру,
  векторна заливка з `ExtGState /ca`. Підсвітка = коробка навколо фігури.
* **Б** — **одне поле з багатьма віджетами-нащадками** (`/Kids`): сітка
  клітинок 6 pt, кожна зі своїм маленьким прямокутником і своїм шматком
  заливки. Значення в полі одне, тож клік по будь-якому нащадку перемикає
  **всю** область (той самий механізм, що в спільного поля «Ім'я»). Підсвітка
  починає окреслювати саму фігуру, а не коробку.
* **В** — растровий трафарет: замість векторного контуру `/ImageMask` (1 біт на
  піксель), побудований заливкою по пікселях від чорних ліній малюнка. Прямокутник
  той самий, що в А (підсвітка не змінюється), але **фарба лягає рівно до лінії**,
  без ореолу від обведеного від руки контуру.

Прозорість у всіх трьох — той самий `ExtGState /ca 0.45`, колір той самий, стан
`Off` — порожній потік. Різниця лише в джерелі форми.

Що виміряно на стор. 16 (сітка кліків 0.5 pt, 23 152 точки в областях):

| | віджетів | правильних кліків | «нічийна» площа |
|---|---|---|---|
| А | 11 | 96.7 % | 16.9 % |
| Б | 216 | 95.0 % | 7.7 % |
| В | 11 | 96.7 % | 16.9 % |

Правило «центр клітинки всередині контуру» (`--cover 0.5`) дає 161 віджет,
84.6 % кліків і 3.3 % «нічийного», але **втрачає 15 % заливки** по периметру:
прикордонні клітинки випадають цілком, і в фігури з'являються виїмки в пів
клітинки. Тому поріг знижено до 0.20 (клітинка лишається, якщо в контурі хоч
20 % її площі): заливка обрізається контуром, тож зайвий колір за фігуру не
виходить — росте лише прямокутник для кліку. Заливка при цьому 96.4 %.

Запуск:
    .venv/bin/python verify/highlight_test.py
    .venv/bin/python verify/highlight_test.py --mark r02,r11 --render 150
"""
import argparse
import collections
import json
import math
import os
import re
import sys

import fitz

SRC = "forms/Level2_UA_form_v06.pdf"
JSON_PATH = "editor/regions_p16_q2.json"
PNO = 15                       # стор. 16
OUTDIR = "verify"
STEM = "highlight_test"
ALPHA = 0.45
GRID = 6.0                     # крок сітки варіанта Б, pt
COVER = 0.20                   # поріг покриття клітинки (0.5 = «центр усередині»)
FONT = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"

RASTER_DPI = 300
INK = 150                      # яскравіше -> «папір», темніше -> лінія
ERODE_PT = 1.6                 # звузити контур, щоб насіння точно було всередині
DILATE_PT = 2.2                # дозволити заливці дійти до лінії, якщо контур не дотяг

TITLES = [
    ("ВАРІАНТ А — один великий прямокутник на область (як зараз)",
     "Дивіться: підсвітка полів малює коробку навколо кожної фігури, і білі кути "
     "коробок теж ловлять клік. 11 полів."),
    ("ВАРІАНТ Б — багато маленьких прямокутників на область",
     "Дивіться: підсвітка складається з клітинок 6 pt і повторює обрис фігури. "
     "Клік у будь-яку клітинку має зафарбувати ВСЮ область — це головне, що треба "
     "перевірити. 11 полів, 216 віджетів. Тонка сітка на самій заливці — артефакт "
     "прозорості на стику клітинок; лікується, підсвітки не стосується."),
    ("ВАРІАНТ В — растровий трафарет, прямокутник як у А",
     "Підсвітка така сама, як у А. Дивіться на саму фарбу: вона лягає рівно до "
     "чорної лінії. Найпомітніше на сандалях і на волоссі. 11 полів."),
]


# ------------------------------------------------------------------ службове
def next_version(d, stem):
    rx = re.compile(r"^%s_v(\d+)\.pdf$" % re.escape(stem))
    mx = 0
    if os.path.isdir(d):
        for fn in os.listdir(d):
            m = rx.match(fn)
            if m:
                mx = max(mx, int(m.group(1)))
    return os.path.join(d, "%s_v%02d.pdf" % (stem, mx + 1))


def hex2rgb(s):
    s = s.lstrip("#")
    return tuple(int(s[i:i + 2], 16) / 255.0 for i in (0, 2, 4))


def bbox(reg):
    xs = [p[0] for path in reg["paths"] for p in path]
    ys = [p[1] for path in reg["paths"] for p in path]
    return fitz.Rect(min(xs), min(ys), max(xs), max(ys))


def poly_area(path):
    s = 0.0
    for i in range(len(path)):
        x0, y0 = path[i]
        x1, y1 = path[(i + 1) % len(path)]
        s += x0 * y1 - x1 * y0
    return abs(s) / 2.0


def region_area(reg):
    return sum(poly_area(p) for p in reg["paths"])


def inside(reg, x, y):
    """Парність перетинів променя — той самий `f*`, що й у потоці вигляду."""
    c = False
    for path in reg["paths"]:
        n = len(path)
        for i in range(n):
            x0, y0 = path[i]
            x1, y1 = path[(i + 1) % n]
            if (y0 > y) != (y1 > y):
                if x < x0 + (y - y0) * (x1 - x0) / (y1 - y0):
                    c = not c
    return c


def path_ops(reg, rect):
    """Контур області в координатах потоку вигляду (`/BBox [0 0 w h]` -> `/Rect`)."""
    out = []
    for path in reg["paths"]:
        if len(path) < 3:
            continue
        out.append("%.2f %.2f m" % (path[0][0] - rect.x0, rect.y1 - path[0][1]))
        for x, y in path[1:]:
            out.append("%.2f %.2f l" % (x - rect.x0, rect.y1 - y))
        out.append("h")
    return " ".join(out)


GS = "/GSa<</Type/ExtGState/ca %.3f/CA %.3f/BM/Normal>>"
PAGE_H = 841.89                # висота сторінки, ставиться при збірці


def form_xobj(doc, w, h, resources, content):
    x = doc.get_new_xref()
    doc.update_object(
        x, "<</Type/XObject/Subtype/Form/FormType 1/BBox[0 0 %.3f %.3f]"
           "/Resources<<%s>>>>" % (w, h, resources))
    doc.update_stream(x, content.encode("latin-1"), new=True)
    return x


def empty_off(doc, w, h):
    return form_xobj(doc, w, h, "", "")


def widget_obj(doc, rect, on, off, extra=""):
    """Віджет-анотація з власним виглядом. Ніякого `widget.update()` (§3.6).

    `rect` — у координатах PyMuPDF (origin ліворуч УГОРІ); у `/Rect` треба
    координати PDF (origin ліворуч УНИЗУ), тож перевертаємо по висоті сторінки.
    Тут це видно тільки тому, що віджет збирається вручну: `page.add_widget()`
    робив би це сам.
    """
    x = doc.get_new_xref()
    doc.update_object(
        x, "<</Type/Annot/Subtype/Widget/F 4/Rect[%.3f %.3f %.3f %.3f]"
           "/AP<</N<</Off %d 0 R/Yes %d 0 R>>>>/AS/Off"
           "/MK<<>>/BS<</W 0/S/S>>/H/N%s>>"
        % (rect.x0, PAGE_H - rect.y1, rect.x1, PAGE_H - rect.y0, off, on, extra))
    return x


# ------------------------------------------------------------------ растр (В)
def scan_mask(reg, x0pt, y0pt, w, h, scale):
    """Бітова маска «точка всередині контуру» по рядках (scanline, точно й швидко)."""
    m = bytearray(w * h)
    edges = []
    for path in reg["paths"]:
        n = len(path)
        for i in range(n):
            edges.append((path[i], path[(i + 1) % n]))
    for iy in range(h):
        y = y0pt + (iy + 0.5) * scale
        xs = []
        for (ax, ay), (bx, by) in edges:
            if (ay > y) != (by > y):
                xs.append(ax + (y - ay) * (bx - ax) / (by - ay))
        if not xs:
            continue
        xs.sort()
        row = iy * w
        for k in range(0, len(xs) - 1, 2):
            a = int((xs[k] - x0pt) / scale)
            b = int((xs[k + 1] - x0pt) / scale)
            for ix in range(max(0, a), min(w, b + 1)):
                x = x0pt + (ix + 0.5) * scale
                if xs[k] <= x <= xs[k + 1]:
                    m[row + ix] = 1
    return m


def morph(m, w, h, r, grow):
    """Квадратна дилатація/ерозія радіусом r px, двома одновимірними проходами."""
    if r <= 0:
        return bytearray(m)
    a = bytearray(m)
    for _ in range(2):                     # прохід по x, потім по y
        b = bytearray(len(a))
        for iy in range(h):
            row = iy * w
            cnt = 0
            for ix in range(min(r, w - 1) + 1):
                cnt += a[row + ix]
            for ix in range(w):
                lo, hi = ix - r, ix + r
                span = min(hi, w - 1) - max(lo, 0) + 1
                b[row + ix] = 1 if (cnt > 0 if grow else
                                    (cnt == span and lo >= 0 and hi <= w - 1)) else 0
                if hi + 1 < w:
                    cnt += a[row + hi + 1]
                if lo >= 0:
                    cnt -= a[row + lo]
        #  транспонуємо, щоб другий прохід був тим самим кодом
        t = bytearray(len(b))
        for iy in range(h):
            for ix in range(w):
                t[ix * h + iy] = b[iy * w + ix]
        a, w, h = t, h, w
    return a


def page_background(paper, pw, ph):
    """Тло сторінки — зв'язна «паперова» область, що виходить на край ділянки.

    Без цього трафарет марний саме там, де він потрібен. Контур «сандалі»
    обведено стилусом грубим восьмикутником, який вилазить за черевик на
    порожній аркуш; заливка по пікселях сама собою залила б і цей аркуш, і —
    через дозволене розширення — розтеклася б далі. Тло виключаємо один раз на
    всю ділянку: фарбуємо лише те, що всередині ліній малюнка.
    """
    bg = bytearray(pw * ph)
    q = collections.deque()
    for i in list(range(pw)) + list(range((ph - 1) * pw, ph * pw)):
        if paper[i] and not bg[i]:
            bg[i] = 1
            q.append(i)
    for iy in range(ph):
        for i in (iy * pw, iy * pw + pw - 1):
            if paper[i] and not bg[i]:
                bg[i] = 1
                q.append(i)
    while q:
        p = q.popleft()
        x, y = p % pw, p // pw
        for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
            if 0 <= nx < pw and 0 <= ny < ph:
                n = ny * pw + nx
                if paper[n] and not bg[n]:
                    bg[n] = 1
                    q.append(n)
    return bg


def raster_mask(reg, ink_ok, pw, ph, ox, oy, scale):
    """Трафарет області: заливка по пікселях від насіння всередині контуру.

    `ink_ok` — «папір, який не є тлом сторінки»: усе, що всередині ліній
    малюнка. Насіння — контур, звужений на `ERODE_PT` (щоб обведення від руки,
    яке перелізло через чорну лінію, не дало насіння на сусідній деталі).
    Розтікання дозволене в межах контуру, розширеного на `DILATE_PT`, і тільки
    по `ink_ok` — тобто зупиняється рівно на чорній лінії малюнка.
    """
    paper = ink_ok
    r = bbox(reg)
    pad = DILATE_PT + 1.0
    x0 = max(0, int((r.x0 - ox - pad) / scale))
    y0 = max(0, int((r.y0 - oy - pad) / scale))
    x1 = min(pw, int((r.x1 - ox + pad) / scale) + 1)
    y1 = min(ph, int((r.y1 - oy + pad) / scale) + 1)
    w, h = x1 - x0, y1 - y0
    P = scan_mask(reg, ox + x0 * scale, oy + y0 * scale, w, h, scale)

    er = max(1, int(round(ERODE_PT / scale)))
    dl = max(1, int(round(DILATE_PT / scale)))
    seeds = morph(P, w, h, er, False)
    if not any(seeds):
        seeds = bytearray(P)
    allow = morph(P, w, h, dl, True)

    out = bytearray(w * h)
    q = collections.deque()
    for i in range(w * h):
        if seeds[i] and paper[(y0 + i // w) * pw + (x0 + i % w)]:
            out[i] = 1
            q.append(i)
    while q:
        p = q.popleft()
        x, y = p % w, p // w
        for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
            if not (0 <= nx < w and 0 <= ny < h):
                continue
            n = ny * w + nx
            if out[n] or not allow[n]:
                continue
            if paper[(y0 + ny) * pw + (x0 + nx)]:
                out[n] = 1
                q.append(n)
    if not any(out):
        for i in range(w * h):
            out[i] = 1 if (P[i] and paper[(y0 + i // w) * pw + (x0 + i % w)]) else 0
    return out, fitz.Rect(ox + x0 * scale, oy + y0 * scale,
                          ox + x1 * scale, oy + y1 * scale), w, h


def image_mask_xobj(doc, mask, w, h):
    """`/ImageMask` 1 біт/піксель: 1 = фарбувати (звідси `/Decode [1 0]`)."""
    rowb = (w + 7) // 8
    data = bytearray(rowb * h)
    for iy in range(h):
        base = iy * rowb
        row = iy * w
        for ix in range(w):
            if mask[row + ix]:
                data[base + (ix >> 3)] |= 0x80 >> (ix & 7)
    x = doc.get_new_xref()
    doc.update_object(
        x, "<</Type/XObject/Subtype/Image/Width %d/Height %d/ImageMask true"
           "/BitsPerComponent 1/Decode[1 0]>>" % (w, h))
    doc.update_stream(x, bytes(data), new=True)
    return x


# ------------------------------------------------------------------ сторінки
def build_page_A(doc, page, regs, alpha, prefix):
    """Один віджет на область; прямокутник = bbox контуру."""
    annots, fields, layer = [], [], []
    for i, reg in enumerate(regs, 1):
        rect = bbox(reg)
        r, g, b = hex2rgb(reg["color"])
        on = form_xobj(doc, rect.width, rect.height, "/ExtGState<<%s>>" % (GS % (alpha, alpha)),
                       "q /GSa gs %.3f %.3f %.3f rg %s f* Q"
                       % (r, g, b, path_ops(reg, rect)))
        off = empty_off(doc, rect.width, rect.height)
        x = widget_obj(doc, rect, on, off,
                       "/FT/Btn/T(%s_art%02d)/V/Off" % (prefix, i))
        annots.append(x)
        fields.append(x)
        layer.append((rect, i - 1))
    return annots, fields, layer


SUB = 4                         # підвибірка 4x4 для оцінки покриття клітинки


def cell_cover(reg, x0, y0, step):
    """Частка клітинки, що лежить усередині контуру (оцінка по сітці 4x4)."""
    n = 0
    for jy in range(SUB):
        y = y0 + (jy + 0.5) * step / SUB
        for jx in range(SUB):
            if inside(reg, x0 + (jx + 0.5) * step / SUB, y):
                n += 1
    return n / float(SUB * SUB)


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

    `thr = 0.5` — правило «центр усередині контуру» (те, що просив замовник);
    менший поріг лишає й прикордонні клітинки, і тоді заливка не втрачає
    смужку по периметру. Заливка все одно обрізається контуром, тож зайвого
    кольору за межі фігури не виходить — росте лише прямокутник для кліку.
    """
    rb = bbox(reg)
    out = []
    for iy in range(int(math.floor(rb.y0 / step)), int(math.ceil(rb.y1 / step)) + 1):
        for ix in range(int(math.floor(rb.x0 / step)), int(math.ceil(rb.x1 / step)) + 1):
            x0, y0 = ix * step, iy * step
            if thr >= 0.5:
                if not inside(reg, x0 + step / 2, y0 + step / 2):
                    continue
            elif cell_cover(reg, x0, y0, step) < thr:
                continue
            out.append(fitz.Rect(x0, y0, x0 + step, y0 + step))
    return out


def build_page_B(doc, page, regs, alpha, prefix, step, thr):
    """Одне поле на область + багато віджетів-нащадків (`/Kids`)."""
    annots, fields, layer = [], [], []
    stat = []
    for i, reg in enumerate(regs, 1):
        r, g, b = hex2rgb(reg["color"])
        kids = []
        for cell in cells_of(reg, step, thr):
            on = form_xobj(doc, cell.width, cell.height,
                           "/ExtGState<<%s>>" % (GS % (alpha, alpha)),
                           "q /GSa gs %.3f %.3f %.3f rg %s f* Q"
                           % (r, g, b, path_ops(reg, cell)))
            off = empty_off(doc, cell.width, cell.height)
            kids.append((widget_obj(doc, cell, on, off), cell))
        if not kids:                       # надто тонка область — лишаємо як у А
            annots_a, fields_a, layer_a = build_page_A(doc, page, [reg], alpha,
                                                       "%s_thin%02d" % (prefix, i))
            annots += annots_a
            fields += fields_a
            layer += [(rc, i - 1) for rc, _ in layer_a]
            stat.append((reg["name"], 1))
            continue
        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, cell in kids:
            doc.xref_set_key(k, "Parent", "%d 0 R" % par)
            annots.append(k)
            layer.append((cell, i - 1))
        fields.append(par)
        stat.append((reg["name"], len(kids)))
    return annots, fields, layer, stat


def build_page_C(doc, page, regs, alpha, prefix, ink_ok, pw, ph, ox, oy, scale):
    """Растровий трафарет; прямокутник той самий, що в А."""
    annots, fields, layer = [], [], []
    stat = []
    for i, reg in enumerate(regs, 1):
        rect = bbox(reg)
        r, g, b = hex2rgb(reg["color"])
        mask, mrect, mw, mh = raster_mask(reg, ink_ok, pw, ph, ox, oy, scale)
        im = image_mask_xobj(doc, mask, mw, mh)
        #  зображення малюється в одиничний квадрат, тож `cm` розтягує його на mrect
        content = ("q /GSa gs %.3f %.3f %.3f rg %.3f 0 0 %.3f %.3f %.3f cm /Im0 Do Q"
                   % (r, g, b, mrect.width, mrect.height,
                      mrect.x0 - rect.x0, rect.y1 - mrect.y1))
        on = form_xobj(doc, rect.width, rect.height,
                       "/ExtGState<<%s>>/XObject<</Im0 %d 0 R>>"
                       % (GS % (alpha, alpha), im), content)
        off = empty_off(doc, rect.width, rect.height)
        x = widget_obj(doc, rect, on, off,
                       "/FT/Btn/T(%s_art%02d)/V/Off" % (prefix, i))
        annots.append(x)
        fields.append(x)
        layer.append((rect, i - 1))
        stat.append((reg["name"], sum(mask) * scale * scale, region_area(reg)))
    return annots, fields, layer, stat


# ------------------------------------------------------------------ вимірювання
def measure(regs, layer, art, step=0.5):
    """Той самий підрахунок, що й `verify/regions_click.py`, але по шару віджетів.

    `layer` — [(прямокутник, індекс області)] у порядку `/Annots`; верхній —
    останній. Справжній власник точки — остання (верхня) область, у контур якої
    точка потрапляє.
    """
    nx = int(art.width / step) + 1
    ny = int(art.height / step) + 1
    tot = own = hit = dead = 0
    for iy in range(ny):
        y = art.y0 + (iy + 0.5) * step
        for ix in range(nx):
            x = art.x0 + (ix + 0.5) * step
            tot += 1
            o = None
            for k, reg in enumerate(regs):
                if inside(reg, x, y):
                    o = k
            c = None
            for rc, k in layer:
                if rc.contains(fitz.Point(x, y)):
                    c = k
            if o is not None:
                own += 1
                if c == o:
                    hit += 1
            elif c is not None:
                dead += 1
    return {"points": tot, "in_region": own, "hit": hit, "dead": dead,
            "hit_pct": 100.0 * hit / max(1, own),
            "dead_pct": 100.0 * dead / max(1, tot)}


# ------------------------------------------------------------------ збірка
def title(page, lines, fontname):
    """Заголовок варіанта замість шапки уроку — тест має бути підписаний."""
    #  шапка уроку закінчується червоною лінійкою на y = 85.5 — її лишаємо
    page.draw_rect(fitz.Rect(18, 12, 578, 82), color=None, fill=(1, 1, 1))
    page.insert_text((28, 40), lines[0], fontname=fontname, fontsize=14,
                     color=(0.85, 0.15, 0.2))
    y = 57
    for chunk in wrap(lines[1], 112):
        page.insert_text((28, y), chunk, fontname=fontname, fontsize=8.5,
                         color=(0.15, 0.15, 0.15))
        y += 11


def wrap(s, n):
    out, cur = [], ""
    for wd in s.split():
        if len(cur) + len(wd) + 1 > n and cur:
            out.append(cur)
            cur = wd
        else:
            cur = (cur + " " + wd).strip()
    if cur:
        out.append(cur)
    return out


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("--json", default=JSON_PATH)
    ap.add_argument("--src", default=SRC)
    ap.add_argument("--out")
    ap.add_argument("--alpha", type=float, default=None)
    ap.add_argument("--grid", type=float, default=GRID)
    ap.add_argument("--cover", type=float, default=COVER,
                    help="поріг покриття клітинки; 0.5 = «центр усередині контуру»")
    ap.add_argument("--mark", default="", help="id областей через кому: r02,r11")
    ap.add_argument("--render", type=int, default=0)
    a = ap.parse_args(argv)

    with open(a.json, encoding="utf-8") as fh:
        j = json.load(fh)
    alpha = a.alpha if a.alpha is not None else j.get("opacity", ALPHA)
    #  той самий порядок шару, що в `regions_apply.py`: за ПРЯМОКУТНИКОМ,
    #  великі знизу — інакше в дрібну деталь не влучиш (§BACKLOG 3).
    regs = sorted(j["regions"],
                  key=lambda r: (bbox(r).get_area(), region_area(r)), reverse=True)
    art = bbox(regs[0])
    for r in regs:
        art |= bbox(r)

    global PAGE_H
    doc = fitz.open()
    for _ in range(3):
        src = fitz.open(a.src)
        doc.insert_pdf(src, from_page=PNO, to_page=PNO, annots=False)
        src.close()
    assert len(doc) == 3
    PAGE_H = doc[0].rect.height
    cs = [doc.xref_get_key(p.xref, "Contents") for p in doc]
    assert len(set(map(str, cs))) == 3, "сторінки поділили один потік: %s" % cs

    fname = doc[0].insert_font(fontfile=FONT, fontname="ArB")
    for p in doc[1:]:
        p.insert_font(fontfile=FONT, fontname="ArB")
    for p, t in zip(doc, TITLES):
        title(p, t, "ArB")

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

    annA, fldA, layA = build_page_A(doc, doc[0], regs, alpha, "va_p16_q2")
    annB, fldB, layB, statB = build_page_B(doc, doc[1], regs, alpha, "vb_p16_q2",
                                           a.grid, a.cover)
    annC, fldC, layC, statC = build_page_C(doc, doc[2], regs, alpha, "vc_p16_q2",
                                           ink_ok, pw, ph, ox, oy, scale)

    ids = {reg["id"]: k for k, reg in enumerate(regs)}
    want = {ids[m.strip()] for m in a.mark.split(",") if m.strip() in ids}

    for page, ann, fld in ((doc[0], annA, fldA), (doc[1], annB, fldB),
                           (doc[2], annC, fldC)):
        doc.xref_set_key(page.xref, "Annots",
                         "[%s]" % " ".join("%d 0 R" % x for x in ann))
    allf = fldA + fldB + fldC
    doc.xref_set_key(doc.pdf_catalog(), "AcroForm",
                     "<</Fields[%s]/NeedAppearances false>>"
                     % " ".join("%d 0 R" % x for x in allf))
    doc.xref_set_key(doc.pdf_catalog(), "AcroForm/NeedAppearances", "false")

    if want:
        #  позначаємо через `/AS` на віджеті + `/V` на полі — ніколи `update()`
        for ann, lay in ((annA, layA), (annB, layB), (annC, layC)):
            for x, (_, k) in zip(ann, lay):
                if k in want:
                    doc.xref_set_key(x, "AS", "/Yes")
        for fld in (fldA, fldB, fldC):     # поля йдуть у порядку областей
            for i, x in enumerate(fld):
                if i in want:
                    doc.xref_set_key(x, "V", "/Yes")

    out = a.out or next_version(OUTDIR, STEM)
    os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
    doc.subset_fonts()
    doc.save(out, deflate=True, garbage=3)
    doc.close()

    # ------------------------------------------------------------- звіт
    print("малюнок: [%.0f %.0f %.0f %.0f] = %.0f pt², областей %d\n"
          % (art.x0, art.y0, art.x1, art.y1, art.get_area(), len(regs)))
    print("варіант Б — нащадків на область (сітка %.1f pt, поріг %.2f):"
          % (a.grid, a.cover))
    for nm, n in statB:
        print("   %-22s %4d" % (nm, n))
    print("   разом %d\n" % sum(n for _, n in statB))
    print("варіант В — площа трафарету проти площі контуру:")
    for nm, ar, pa in statC:
        print("   %-22s %7.1f pt² проти %7.1f pt² (%.0f %%)"
              % (nm, ar, pa, 100.0 * ar / max(1e-6, pa)))
    print()
    res = {}
    for tag, lay in (("А", layA), ("Б", layB), ("В", layC)):
        res[tag] = measure(regs, lay, art)
        m = res[tag]
        print("%s: віджетів %3d | правильних кліків %5.1f %% (%d з %d) | "
              "«нічийна» площа %5.1f %%"
              % (tag, len(lay), m["hit_pct"], m["hit"], m["in_region"], m["dead_pct"]))
    print("\n%s: %.0f КБ" % (out, os.path.getsize(out) / 1024))

    if a.render:
        d = fitz.open(out)
        stem = os.path.splitext(os.path.basename(out))[0]
        for i in range(3):
            fn = os.path.join(OUTDIR, "%s_p%d.png" % (stem, i + 1))
            d[i].get_pixmap(dpi=a.render).save(fn)
            print("рендер:", fn)
        d.close()
    return 0


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