#!/usr/bin/env python3
"""Build the deterministic Twitch profile-picture study.

The source is a 1200 x 800 RGBA grid with four corner markers, a central
subject, one-pixel rules and two text sizes. The browser tool and this script
use the same continuous Fill/Fit geometry for a 256 x 256 canvas. The generated
Fit raster rounds the continuous 170.667-row drawing to 171 rows at y = 43;
the browser draws the fractional 170.667 rows at y = 42.667 before its own
rasterisation.

PNG files are written with Python's standard library. JPG files are made from
the same RGB pixels with the local `cjpeg` command at quality 92. Every output
is read back: every PNG is unfiltered, CRC-checked and compared with its
expected RGBA pixels; JPEG dimensions are parsed separately from the marker
stream, and a separate `djpeg` decode supplies the actual decoded RGB colours.
There is no clock, randomness or network access.
"""

import hashlib
import json
import math
import os
import struct
import subprocess
import tempfile
import zlib

HERE = os.path.dirname(os.path.abspath(__file__))
SRC_W, SRC_H = 1200, 800
SIZE = 256
SMALL_LABEL_Y0, SMALL_LABEL_Y1 = 440, 453

BG = (24, 18, 38, 255)
GRID = (66, 53, 88, 255)
GRID_MAJOR = (103, 80, 136, 255)
INK = (246, 240, 255, 255)
PURPLE = (124, 86, 234, 255)
LIME = (203, 255, 92, 255)
TEXT = (255, 255, 255, 255)
MARKERS = [(255, 104, 92, 255), (74, 212, 255, 255), (255, 203, 82, 255), (135, 240, 154, 255)]

FONT = {
    " ": ["00000"] * 7,
    "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
    "2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
    "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
    "5": ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
    "6": ["00110", "01000", "10000", "11110", "10001", "10001", "01110"],
    "A": ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
    "E": ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
    "F": ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
    "I": ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
    "L": ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
    "M": ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
    "P": ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
    "S": ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
    "X": ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
    "Z": ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
    "/": ["00001", "00010", "00010", "00100", "01000", "01000", "10000"],
}


def canvas(width, height, colour=(0, 0, 0, 0)):
    row = bytes(colour) * width
    return [bytearray(row) for _ in range(height)]


def pixel(px, width, height, x, y, colour):
    if 0 <= x < width and 0 <= y < height:
        px[y][x * 4:x * 4 + 4] = bytes(colour)


def rect(px, width, height, x0, y0, w, h, colour):
    for y in range(max(0, y0), min(height, y0 + h)):
        for x in range(max(0, x0), min(width, x0 + w)):
            pixel(px, width, height, x, y, colour)


def disc(px, width, height, cx, cy, radius, colour):
    r2 = radius * radius
    for y in range(max(0, cy - radius), min(height, cy + radius + 1)):
        for x in range(max(0, cx - radius), min(width, cx + radius + 1)):
            if (x - cx) ** 2 + (y - cy) ** 2 <= r2:
                pixel(px, width, height, x, y, colour)


def text(px, width, height, x, y, value, scale, colour):
    cursor = x
    for char in value:
        for gy, line in enumerate(FONT[char]):
            for gx, bit in enumerate(line):
                if bit == "1":
                    rect(px, width, height, cursor + gx * scale, y + gy * scale, scale, scale, colour)
        cursor += 6 * scale


def centered_text(px, width, height, y, value, scale, colour):
    x = (width - (len(value) * 6 * scale - scale)) // 2
    text(px, width, height, x, y, value, scale, colour)


def source_image():
    px = canvas(SRC_W, SRC_H, BG)
    for x in range(0, SRC_W, 100):
        rect(px, SRC_W, SRC_H, x, 0, 1, SRC_H, GRID_MAJOR if x % 400 == 0 else GRID)
    for y in range(0, SRC_H, 100):
        rect(px, SRC_W, SRC_H, 0, y, SRC_W, 1, GRID_MAJOR if y % 400 == 0 else GRID)
    rect(px, SRC_W, SRC_H, 0, 0, SRC_W, 10, INK)
    rect(px, SRC_W, SRC_H, 0, SRC_H - 10, SRC_W, 10, INK)
    rect(px, SRC_W, SRC_H, 0, 0, 10, SRC_H, INK)
    rect(px, SRC_W, SRC_H, SRC_W - 10, 0, 10, SRC_H, INK)
    disc(px, SRC_W, SRC_H, 600, 400, 190, PURPLE)
    disc(px, SRC_W, SRC_H, 600, 400, 118, BG)
    rect(px, SRC_W, SRC_H, 599, 160, 2, 480, LIME)
    rect(px, SRC_W, SRC_H, 360, 399, 480, 2, LIME)
    for (x, y), colour in zip([(32, 32), (1104, 32), (32, 704), (1104, 704)], MARKERS):
        rect(px, SRC_W, SRC_H, x, y, 64, 64, colour)
    centered_text(px, SRC_W, SRC_H, 310, "PFP / SIZE", 10, TEXT)
    centered_text(px, SRC_W, SRC_H, 440, "SMALL 14 PX", 2, TEXT)
    return px


def small_label_mask(pixels):
    """Return the white source pixels only in the declared small-label band."""
    mask = {
        (x, y)
        for y in range(SMALL_LABEL_Y0, SMALL_LABEL_Y1 + 1)
        for x in range(SRC_W)
        if bytes(pixels[y][x * 4:x * 4 + 4]) == bytes(TEXT)
    }
    if not mask:
        raise ValueError("small-label mask is empty")
    return mask


def resize(src, width, height, x0, y0, source_width, source_height, out_width, out_height):
    """Nearest-neighbour sampling from a continuous source rectangle."""
    out = canvas(out_width, out_height)
    for y in range(out_height):
        sy = int(math.floor(y0 + (y + 0.5) * source_height / out_height))
        sy = min(height - 1, max(0, sy))
        for x in range(out_width):
            sx = int(math.floor(x0 + (x + 0.5) * source_width / out_width))
            sx = min(width - 1, max(0, sx))
            out[y][x * 4:x * 4 + 4] = src[sy][sx * 4:sx * 4 + 4]
    return out


def source_coord(x, y, out_width, out_height, x0, y0, source_width, source_height):
    sx = int(math.floor(x0 + (x + 0.5) * source_width / out_width))
    sy = int(math.floor(y0 + (y + 0.5) * source_height / out_height))
    return min(SRC_W - 1, max(0, sx)), min(SRC_H - 1, max(0, sy))


def export_source_coord(mode, geometry, x, y):
    """Map one output pixel back to the source, using the generator geometry."""
    if mode == "fill":
        return source_coord(
            x, y, SIZE, SIZE,
            geometry["source_x"], geometry["source_y"],
            geometry["source_region_px"][0], geometry["source_region_px"][1],
        )
    x0, y0 = geometry["draw_offset_px"]
    raster_w, raster_h = geometry["drawn_raster_px"]
    if not (x0 <= x < x0 + raster_w and y0 <= y < y0 + raster_h):
        return None
    return source_coord(
        x - x0, y - y0, raster_w, raster_h, 0, 0, SRC_W, SRC_H
    )


def small_label_rows(pixels, width, height, mode, geometry, mask, target=None):
    """Count output rows containing a white pixel sampled from the small mask."""
    rows = set()
    for y in range(height):
        for x in range(width):
            if bytes(pixels[y][x * 4:x * 4 + 4]) != bytes(TEXT):
                continue
            if target is None:
                ox, oy = x, y
            else:
                ox = int(math.floor((x + 0.5) * SIZE / target))
                oy = int(math.floor((y + 0.5) * SIZE / target))
            source = export_source_coord(mode, geometry, ox, oy)
            if source in mask:
                rows.add(y)
    return len(rows)


def rgba_bytes(pixels):
    return b"".join(bytes(row) for row in pixels)


def fill_export(src):
    scale = max(SIZE / SRC_W, SIZE / SRC_H)
    kept_w = SIZE / scale
    kept_h = SIZE / scale
    x0 = (SRC_W - kept_w) / 2
    y0 = (SRC_H - kept_h) / 2
    out = resize(src, SRC_W, SRC_H, x0, y0, kept_w, kept_h, SIZE, SIZE)
    removed = SRC_W * SRC_H - kept_w * kept_h
    return out, {
        "scale": scale,
        "source_region_px": [kept_w, kept_h],
        "source_x": x0,
        "source_y": y0,
        "crop_area_px": removed,
        "crop_share": removed / (SRC_W * SRC_H),
        "pad_area_px": 0,
        "pad_share": 0,
    }


def fit_export(src):
    scale = min(SIZE / SRC_W, SIZE / SRC_H)
    drawn_w = SRC_W * scale
    drawn_h = SRC_H * scale
    raster_w = int(round(drawn_w))
    raster_h = int(round(drawn_h))
    x0 = int(round((SIZE - drawn_w) / 2))
    y0 = int(round((SIZE - drawn_h) / 2))
    drawn = resize(src, SRC_W, SRC_H, 0, 0, SRC_W, SRC_H, raster_w, raster_h)
    out = canvas(SIZE, SIZE)
    for y, row in enumerate(drawn):
        if 0 <= y0 + y < SIZE:
            start = x0 * 4
            out[y0 + y][start:start + len(row)] = row
    pad_area = SIZE * SIZE - drawn_w * drawn_h
    return out, {
        "scale": scale,
        "source_region_px": [SRC_W, SRC_H],
        "source_x": 0,
        "source_y": 0,
        "crop_area_px": 0,
        "crop_share": 0,
        "pad_area_px": pad_area,
        "pad_share": pad_area / (SIZE * SIZE),
        "draw_offset_px": [x0, y0],
        "drawn_raster_px": [raster_w, raster_h],
    }


def preview(src, width, height, target):
    out = canvas(target, target)
    for y in range(target):
        sy = int(math.floor((y + 0.5) * height / target))
        for x in range(target):
            sx = int(math.floor((x + 0.5) * width / target))
            out[y][x * 4:x * 4 + 4] = src[sy][sx * 4:sx * 4 + 4]
    return out


def write_png(path, pixels, width, height):
    raw = b"".join(b"\x00" + bytes(row) for row in pixels)

    def chunk(kind, data):
        return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)

    with open(path, "wb") as stream:
        stream.write(b"\x89PNG\r\n\x1a\n")
        stream.write(chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)))
        stream.write(chunk(b"IDAT", zlib.compress(raw, 9)))
        stream.write(chunk(b"IEND", b""))


def write_ppm(path, pixels, width, height):
    with open(path, "wb") as stream:
        stream.write(f"P6\n{width} {height}\n255\n".encode("ascii"))
        for row in pixels:
            rgb = bytearray(width * 3)
            for x in range(width):
                source = row[x * 4:x * 4 + 4]
                alpha = source[3] / 255
                rgb[x * 3:x * 3 + 3] = bytes(
                    int(round(source[channel] * alpha + 255 * (1 - alpha)))
                    for channel in range(3)
                )
            stream.write(rgb)


def png_decode(path):
    data = open(path, "rb").read()
    if data[:8] != b"\x89PNG\r\n\x1a\n":
        raise ValueError("not a PNG: " + path)
    offset = 8
    chunks = []
    idat = []
    width = height = None
    colour_type = None
    while offset < len(data):
        length = struct.unpack(">I", data[offset:offset + 4])[0]
        kind = data[offset + 4:offset + 8]
        body = data[offset + 8:offset + 8 + length]
        expected = struct.unpack(">I", data[offset + 8 + length:offset + 12 + length])[0]
        if zlib.crc32(kind + body) & 0xFFFFFFFF != expected:
            raise ValueError("bad PNG chunk CRC: " + path)
        if kind == b"IHDR":
            width, height = struct.unpack(">II", body[:8])
            colour_type = body[9]
        if kind == b"IDAT":
            idat.append(body)
        chunks.append(kind.decode("ascii"))
        offset += length + 12
    raster = zlib.decompress(b"".join(idat))
    if colour_type != 6:
        raise ValueError("expected RGBA PNG: " + path)
    stride = width * 4
    expected_length = height * (stride + 1)
    if len(raster) != expected_length:
        raise ValueError("decoded PNG length mismatch: " + path)
    decoded = bytearray(height * stride)
    previous = bytearray(stride)
    position = 0
    filter_types = set()
    for y in range(height):
        filter_type = raster[position]
        position += 1
        filter_types.add(filter_type)
        row = bytearray(raster[position:position + stride])
        position += stride
        for i in range(stride):
            left = row[i - 4] if i >= 4 else 0
            above = previous[i]
            upper_left = previous[i - 4] if i >= 4 else 0
            if filter_type == 0:
                value = row[i]
            elif filter_type == 1:
                value = row[i] + left
            elif filter_type == 2:
                value = row[i] + above
            elif filter_type == 3:
                value = row[i] + ((left + above) // 2)
            elif filter_type == 4:
                predictor = left + above - upper_left
                pa, pb, pc = abs(predictor - left), abs(predictor - above), abs(predictor - upper_left)
                value = row[i] + (left if pa <= pb and pa <= pc else above if pb <= pc else upper_left)
            else:
                raise ValueError(f"unsupported PNG filter {filter_type}: {path}")
            row[i] = value & 0xFF
        decoded[y * stride:(y + 1) * stride] = row
        previous = row
    return width, height, colour_type, sorted(filter_types), chunks, bytes(decoded)


def read_png(path, expected_pixels=None):
    width, height, colour_type, filter_types, chunks, decoded = png_decode(path)
    decoded_sha = hashlib.sha256(decoded).hexdigest()
    info = {
        "width": width,
        "height": height,
        "colour_type": "RGBA",
        "chunk_crc_ok": True,
        "filter_types": filter_types,
        "chunks": chunks,
        "decoded_rgba_bytes": len(decoded),
        "decoded_rgba_sha256": decoded_sha,
    }
    if expected_pixels is not None:
        expected = rgba_bytes(expected_pixels)
        expected_sha = hashlib.sha256(expected).hexdigest()
        info["expected_rgba_sha256"] = expected_sha
        info["pixel_match"] = decoded == expected
        if not info["pixel_match"]:
            raise ValueError("decoded PNG pixels do not match expected RGBA: " + path)
    return info


def read_ppm(path):
    data = open(path, "rb").read()
    if not data.startswith(b"P6\n"):
        raise ValueError("expected binary PPM: " + path)
    parts = data.split(b"\n", 3)
    if len(parts) != 4:
        raise ValueError("incomplete PPM header: " + path)
    width, height = map(int, parts[1].split())
    if int(parts[2]) != 255:
        raise ValueError("expected 8-bit PPM: " + path)
    rgb = parts[3]
    if len(rgb) != width * height * 3:
        raise ValueError("PPM payload length mismatch: " + path)
    return width, height, rgb


def count_rgb_colour(rgb, width, height, colours):
    wanted = {bytes(colour[:3]) for colour in colours}
    return sum(
        rgb[(y * width + x) * 3:(y * width + x) * 3 + 3] in wanted
        for y in range(height)
        for x in range(width)
    )


def decode_jpeg(path):
    with tempfile.TemporaryDirectory() as directory:
        ppm_path = os.path.join(directory, "decoded.ppm")
        subprocess.run(
            ["djpeg", "-outfile", ppm_path, path],
            check=True,
            capture_output=True,
            text=True,
        )
        width, height, rgb = read_ppm(ppm_path)
    return {
        "decoder": "djpeg",
        "decoded": True,
        "width": width,
        "height": height,
        "rgb_bytes": len(rgb),
        "rgb_sha256": hashlib.sha256(rgb).hexdigest(),
        "marker_exact_pixels": count_rgb_colour(rgb, width, height, MARKERS),
        "method": "djpeg decodes the JPG to P6 RGB; exact marker RGB pixels are counted after decoding",
    }


def read_jpeg(path):
    data = open(path, "rb").read()
    if data[:2] != b"\xff\xd8":
        raise ValueError("not a JPEG: " + path)
    offset = 2
    width = height = None
    while offset + 4 <= len(data):
        if data[offset] != 0xFF:
            offset += 1
            continue
        marker = data[offset + 1]
        offset += 2
        if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
            continue
        length = struct.unpack(">H", data[offset:offset + 2])[0]
        if marker in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF):
            height, width = struct.unpack(">HH", data[offset + 3:offset + 7])
            break
        offset += length
    if not width or not height:
        raise ValueError("JPEG dimensions not found: " + path)
    return {
        "width": width,
        "height": height,
        "sof_marker_found": True,
        "has_eoi": data.endswith(b"\xff\xd9"),
        "file_bytes_read": len(data),
    }


def digest(path):
    return hashlib.sha256(open(path, "rb").read()).hexdigest()


def count_colour(pixels, width, height, colours):
    wanted = {bytes(colour) for colour in colours}
    return sum(bytes(pixels[y][x * 4:x * 4 + 4]) in wanted for y in range(height) for x in range(width))


def main():
    exports_dir = os.path.join(HERE, "exports")
    previews_dir = os.path.join(HERE, "previews")
    circle_dir = os.path.join(HERE, "circle")
    os.makedirs(exports_dir, exist_ok=True)
    os.makedirs(previews_dir, exist_ok=True)
    os.makedirs(circle_dir, exist_ok=True)

    src = source_image()
    small_mask = small_label_mask(src)
    small_x = [point[0] for point in small_mask]
    small_y = [point[1] for point in small_mask]
    source_path = os.path.join(HERE, "source-grid-1200x800.png")
    write_png(source_path, src, SRC_W, SRC_H)
    source_info = read_png(source_path, src)
    png_readbacks = [{"file": "source-grid-1200x800.png", "role": "source", **source_info}]

    rows = []
    for mode, build in (("fill", fill_export), ("fit", fit_export)):
        pixels, geometry = build(src)
        png_path = os.path.join(exports_dir, f"{mode}-256x256.png")
        write_png(png_path, pixels, SIZE, SIZE)
        png_info = read_png(png_path, pixels)

        ppm_path = os.path.join(exports_dir, f"{mode}-256x256.ppm")
        jpg_path = os.path.join(exports_dir, f"{mode}-256x256.jpg")
        write_ppm(ppm_path, pixels, SIZE, SIZE)
        subprocess.run(["cjpeg", "-quality", "92", "-outfile", jpg_path, ppm_path], check=True)
        os.remove(ppm_path)
        jpeg_decode = decode_jpeg(jpg_path)

        preview_rows = {}
        for target in (32, 64, 128):
            preview_pixels = preview(pixels, SIZE, SIZE, target)
            preview_path = os.path.join(previews_dir, f"{mode}-{target}.png")
            write_png(preview_path, preview_pixels, target, target)
            preview_info = read_png(preview_path, preview_pixels)
            preview_rows[str(target)] = {
                "file": f"previews/{mode}-{target}.png",
                "bytes": os.path.getsize(preview_path),
                "sha256": digest(preview_path),
                "small_label_rows": small_label_rows(
                    preview_pixels, target, target, mode, geometry, small_mask, target
                ),
                "readback": preview_info,
            }
            png_readbacks.append({
                "file": f"previews/{mode}-{target}.png",
                "role": f"{mode}-preview-{target}",
                **preview_info,
            })

        png_bytes = os.path.getsize(png_path)
        jpg_bytes = os.path.getsize(jpg_path)
        marker_before = count_colour(pixels, SIZE, SIZE, MARKERS)
        row = {
            "mode": mode,
            "png": {
                "file": f"exports/{mode}-256x256.png",
                "bytes": png_bytes,
                "sha256": digest(png_path),
                "readback": png_info,
            },
            "jpg": {
                "file": f"exports/{mode}-256x256.jpg",
                "bytes": jpg_bytes,
                "sha256": digest(jpg_path),
                "readback": read_jpeg(jpg_path),
                "decode": jpeg_decode,
            },
            **geometry,
            "transparent_pixels_png": sum(pixels[y][x * 4 + 3] == 0 for y in range(SIZE) for x in range(SIZE)),
            "small_label_rows_before_encode": small_label_rows(
                pixels, SIZE, SIZE, mode, geometry, small_mask
            ),
            "marker_pixels_before_encode": marker_before,
            "marker_pixels_png_exact": marker_before,
            "marker_pixels_jpg_decoded_exact": jpeg_decode["marker_exact_pixels"],
            "previews": preview_rows,
        }
        rows.append(row)
        png_readbacks.append({
            "file": f"exports/{mode}-256x256.png",
            "role": f"{mode}-export",
            **png_info,
        })

    fill_pixels = next(row for row in rows if row["mode"] == "fill")
    fill_file = os.path.join(exports_dir, "fill-256x256.png")
    _, _, _, _, _, fill_decoded = png_decode(fill_file)
    fill_rows = [
        fill_decoded[y * SIZE * 4:(y + 1) * SIZE * 4]
        for y in range(SIZE)
    ]
    circle_pixels = []
    outside = 0
    for y in range(SIZE):
        row = bytearray(SIZE * 4)
        for x in range(SIZE):
            inside = (x + 0.5 - SIZE / 2) ** 2 + (y + 0.5 - SIZE / 2) ** 2 <= (SIZE / 2) ** 2
            if inside:
                row[x * 4:x * 4 + 4] = fill_rows[y][x * 4:x * 4 + 4]
            else:
                outside += 1
        circle_pixels.append(row)
    circle_path = os.path.join(circle_dir, "fill-circle-256x256.png")
    write_png(circle_path, circle_pixels, SIZE, SIZE)
    circle_info = read_png(circle_path, circle_pixels)
    png_readbacks.append({
        "file": "circle/fill-circle-256x256.png",
        "role": "circle-mask",
        **circle_info,
    })

    result = {
        "generator": "make_samples.py",
        "jpeg_encoder": "cjpeg -quality 92",
        "jpeg_decoder": "djpeg",
        "jpeg_decode_method": "djpeg decodes each JPG to P6 RGB; marker exact-colour counts are read from that decoded raster, not from the SOF marker parser",
        "png_readback_method": "parse PNG chunks and CRCs, unfilter every row, compare decoded RGBA bytes with the expected pixels",
        "method": "nearest-neighbour sampling of the documented continuous crop box; browser resampling differs",
        "fit_rasterisation": "generated Fit rounds the continuous 170.667-row drawing to 171 rows at y = 43; the browser draws fractional rows at y = 42.667 before its own rasterisation",
        "source": {
            "file": "source-grid-1200x800.png",
            "width": SRC_W,
            "height": SRC_H,
            "bytes": os.path.getsize(source_path),
            "sha256": digest(source_path),
            "png_readback": source_info,
        },
        "small_label": {
            "text": "SMALL 14 PX",
            "source_band_y": [SMALL_LABEL_Y0, SMALL_LABEL_Y1],
            "source_mask_white_pixels": len(small_mask),
            "source_mask_bbox_xyxy": [min(small_x), min(small_y), max(small_x), max(small_y)],
            "measurement": "rows count only exact-white output pixels whose sampled source coordinate is in this mask; the large PFP / SIZE line is outside the band",
        },
        "canvas": {
            "width": SIZE,
            "height": SIZE,
            "ratio": "1:1",
            "status": "tool canvas; Twitch does not publish a profile-picture ratio or circular-display rule on the pages read",
        },
        "exports": rows,
        "png_readbacks": png_readbacks,
        "circle": {
            "mask_file": "circle/fill-circle-256x256.png",
            "canvas_pixels": SIZE * SIZE,
            "corner_pixels_excluded": outside,
            "actual_share": outside / (SIZE * SIZE),
            "geometric_share": 1 - math.pi / 4,
            "method": "pixel centres outside radius 128 px from the 256 × 256 centre",
            "source_export_bytes": fill_pixels["png"]["bytes"],
            "corner_alpha_zero": all(
                circle_pixels[y][x * 4 + 3] == 0
                for y in range(SIZE)
                for x in range(SIZE)
                if (x + 0.5 - SIZE / 2) ** 2 + (y + 0.5 - SIZE / 2) ** 2 > (SIZE / 2) ** 2
            ),
            "readback": circle_info,
        },
    }
    with open(os.path.join(HERE, "results.json"), "w", encoding="utf-8") as stream:
        json.dump(result, stream, indent=2)
        stream.write("\n")
    print(f"wrote source, 4 exports, 6 previews, circle mask and results.json; outside={outside}")


if __name__ == "__main__":
    main()
