#!/usr/bin/env python3
"""Build the deterministic Reddit banner crop study.

Two synthetic sources are cropped to the two published community-banner
canvases (1072 x 128 and 1080 x 128) with the same continuous Fill/Fit geometry
the browser tool uses. Every export is written twice, as PNG and as JPG, and
every file is read back: PNG chunks are CRC-checked and unfiltered to RGBA,
JPEG dimensions are parsed from the marker stream and the pixels are decoded
with `djpeg`.

The small-label figures are mask counts. A source mask records the pure-white
label pixels first; each export pixel is then mapped back to its source
coordinate and counted only if that coordinate is inside the mask. The result
does not reuse the source label position.

Two negative controls prove the checks can fail: one corrupts a PNG byte and
expects the CRC check to raise, one shifts the crop by 0.001 of the source
height and expects the pixel comparison to report a difference.

There is no clock, randomness or network access. Python standard library plus
the local `cjpeg` and `djpeg` commands.
"""

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

HERE = os.path.dirname(os.path.abspath(__file__))

SOURCES = [
    {"id": "a", "width": 1536, "height": 627, "file": "sources/source-a-1536x627.png",
     "note": "the shape of the 2026-01-23 public post image, redrawn here from code"},
    {"id": "b", "width": 2400, "height": 800, "file": "sources/source-b-2400x800.png",
     "note": "a second, wider source so the same canvases can be compared"},
]

CANVASES = [
    {"id": "desktop", "label": "Community desktop", "width": 1072, "height": 128},
    {"id": "mobile", "label": "Community mobile", "width": 1080, "height": 128},
]

LABEL = "SMALL 14 PX"
LABEL_SCALE = 2
LABEL_BAND = {"a": (306, 319), "b": (40, 53)}

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)]
MARKER_SIZE = 48

FONT = {
    " ": ["00000"] * 7,
    "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
    "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
    "2": ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
    "3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
    "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
    "5": ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
    "6": ["00110", "01000", "10000", "11110", "10001", "10001", "01110"],
    "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
    "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
    "9": ["01110", "10001", "10001", "01111", "00001", "00010", "01100"],
    "A": ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
    "B": ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
    "E": ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
    "I": ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
    "L": ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
    "M": ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
    "N": ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
    "P": ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
    "R": ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
    "S": ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
    "X": ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
    "/": ["00001", "00010", "00010", "00100", "01000", "01000", "10000"],
}


def rel(path):
    """Stable, host-independent label for a file in this directory."""
    return os.path.relpath(path, HERE).replace(os.sep, "/")


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(spec):
    """Build one source: rules, four corner markers, a subject and one small label."""
    width, height = spec["width"], spec["height"]
    px = canvas(width, height, BG)
    step = 96 if width < 1800 else 120
    for x in range(0, width, step):
        rect(px, width, height, x, 0, 1, height, GRID_MAJOR if x % (step * 4) == 0 else GRID)
    for y in range(0, height, step):
        rect(px, width, height, 0, y, width, 1, GRID_MAJOR if y % (step * 4) == 0 else GRID)
    rect(px, width, height, 0, 0, width, 6, INK)
    rect(px, width, height, 0, height - 6, width, 6, INK)
    rect(px, width, height, 0, 0, 6, height, INK)
    rect(px, width, height, width - 6, 0, 6, height, INK)

    cx, cy = width // 2, height // 2
    disc(px, width, height, cx, cy, min(width, height) // 4, PURPLE)
    disc(px, width, height, cx, cy, min(width, height) // 8, BG)
    rect(px, width, height, cx - 1, cy - height // 3, 2, height // 3 * 2, LIME)
    rect(px, width, height, cx - width // 6, cy - 1, width // 3, 2, LIME)

    corners = [
        (24, 24), (width - 24 - MARKER_SIZE, 24),
        (24, height - 24 - MARKER_SIZE), (width - 24 - MARKER_SIZE, height - 24 - MARKER_SIZE),
    ]
    for (x, y), colour in zip(corners, MARKERS):
        rect(px, width, height, x, y, MARKER_SIZE, MARKER_SIZE, colour)

    centered_text(px, width, height, LABEL_BAND[spec["id"]][0] - 120, f"BANNER {width} X {height}", 6, TEXT)
    centered_text(px, width, height, LABEL_BAND[spec["id"]][0], LABEL, LABEL_SCALE, TEXT)
    return px


def small_label_mask(pixels, spec):
    """Pure-white source pixels inside the declared small-label band only."""
    y0, y1 = LABEL_BAND[spec["id"]]
    mask = {
        (x, y)
        for y in range(y0, y1 + 1)
        for x in range(spec["width"])
        if bytes(pixels[y][x * 4:x * 4 + 4]) == bytes(TEXT)
    }
    if not mask:
        raise ValueError("small-label mask is empty for source " + spec["id"])
    return mask


def resize(src, x0, y0, source_width, source_height, out_width, out_height):
    """Nearest-neighbour sampling of a continuous source rectangle."""
    height = len(src)
    width = len(src[0]) // 4
    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, limit_w, limit_h):
    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(limit_w - 1, max(0, sx)), min(limit_h - 1, max(0, sy))


def fill_geometry(spec, target_width, target_height):
    width, height = spec["width"], spec["height"]
    scale = max(target_width / width, target_height / height)
    kept_w, kept_h = target_width / scale, target_height / scale
    x0, y0 = (width - kept_w) / 2, (height - kept_h) / 2
    removed = width * height - kept_w * kept_h
    rows = sorted({
        int(math.floor(y0 + (y + 0.5) * kept_h / target_height))
        for y in range(target_height)
    })
    return {
        "mode": "fill",
        "scale": scale,
        "source_region_px": [kept_w, kept_h],
        "source_x": x0,
        "source_y": y0,
        "source_rows_sampled": len(rows),
        "source_row_first": rows[0],
        "source_row_last": rows[-1],
        "crop_area_px": removed,
        "crop_share": removed / (width * height),
        "pad_area_px": 0,
        "pad_share": 0,
    }


def fill_export(src, spec, target_width, target_height, shift=0.0):
    geometry = fill_geometry(spec, target_width, target_height)
    height = spec["height"]
    y0 = geometry["source_y"] + shift * height
    y0 = max(0.0, min(height - geometry["source_region_px"][1], y0))
    pixels = resize(
        src, geometry["source_x"], y0,
        geometry["source_region_px"][0], geometry["source_region_px"][1],
        target_width, target_height,
    )
    return pixels, dict(geometry, applied_source_y=y0)


def fit_geometry(spec, target_width, target_height):
    width, height = spec["width"], spec["height"]
    scale = min(target_width / width, target_height / height)
    drawn_w, drawn_h = width * scale, height * scale
    raster_w, raster_h = int(round(drawn_w)), int(round(drawn_h))
    x0, y0 = int(round((target_width - drawn_w) / 2)), int(round((target_height - drawn_h) / 2))
    pad = target_width * target_height - drawn_w * drawn_h
    return {
        "mode": "fit",
        "scale": scale,
        "source_region_px": [float(width), float(height)],
        "source_x": 0,
        "source_y": 0,
        "draw_offset_px": [x0, y0],
        "drawn_raster_px": [raster_w, raster_h],
        "drawn_continuous_px": [drawn_w, drawn_h],
        "crop_area_px": 0,
        "crop_share": 0,
        "pad_area_px": pad,
        "pad_share": pad / (target_width * target_height),
    }


def fit_export(src, spec, target_width, target_height):
    geometry = fit_geometry(spec, target_width, target_height)
    raster_w, raster_h = geometry["drawn_raster_px"]
    x0, y0 = geometry["draw_offset_px"]
    drawn = resize(src, 0, 0, spec["width"], spec["height"], raster_w, raster_h)
    out = canvas(target_width, target_height)
    for y, row in enumerate(drawn):
        if 0 <= y0 + y < target_height:
            out[y0 + y][x0 * 4:x0 * 4 + len(row)] = row
    return out, geometry


def export_source_coord(spec, geometry, x, y):
    """Map one export pixel back to the source with the generator geometry."""
    if geometry["mode"] == "fill":
        return source_coord(
            x, y, CANVAS_W, CANVAS_H,
            geometry["source_x"], geometry["applied_source_y"],
            geometry["source_region_px"][0], geometry["source_region_px"][1],
            spec["width"], spec["height"],
        )
    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, spec["width"], spec["height"],
        spec["width"], spec["height"],
    )


def label_rows(pixels, height, width, spec, geometry, mask):
    """Export rows holding a pure-white pixel whose source coordinate is in the 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 export_source_coord(spec, geometry, x, y) in mask:
                rows.add(y)
    return len(rows)


def preview_label_stats(preview_pixels, preview_width, preview_height, export_width, export_height, spec, geometry, mask):
    """Map a preview pixel to the export pixel it sampled, then to the source mask.

    The preview is built with export_x = floor((x + 0.5) * export_width / preview_width)
    and the same expression for y, so this inverse uses those exact coordinates.
    Rows are counted in the preview raster, not in source rows.
    """
    rows, pixels = set(), 0
    for y in range(preview_height):
        export_y = int(math.floor((y + 0.5) * export_height / preview_height))
        for x in range(preview_width):
            if bytes(preview_pixels[y][x * 4:x * 4 + 4]) != bytes(TEXT):
                continue
            export_x = int(math.floor((x + 0.5) * export_width / preview_width))
            if export_source_coord(spec, geometry, export_x, export_y) in mask:
                rows.add(y)
                pixels += 1
    return {"label_rows": len(rows), "label_mask_pixels": pixels}


def marker_stats(pixels, width, height):
    """Count exact marker colours and how many of the four markers survive."""
    wanted = {bytes(colour): index for index, colour in enumerate(MARKERS)}
    counts = [0] * len(MARKERS)
    for y in range(height):
        for x in range(width):
            index = wanted.get(bytes(pixels[y][x * 4:x * 4 + 4]))
            if index is not None:
                counts[index] += 1
    return counts, sum(1 for value in counts if value)


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


def preview(pixels, width, height, target_width):
    target_height = max(1, int(round(height * target_width / width)))
    out = canvas(target_width, target_height)
    for y in range(target_height):
        sy = int(math.floor((y + 0.5) * height / target_height))
        for x in range(target_width):
            sx = int(math.floor((x + 0.5) * width / target_width))
            out[y][x * 4:x * 4 + 4] = pixels[sy][sx * 4:sx * 4 + 4]
    return out, target_height


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, background=(255, 255, 255)):
    """Flatten RGBA onto one background colour, the way a JPG export must."""
    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 + background[channel] * (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: " + rel(path))
    offset, chunks, idat = 8, [], []
    width = height = 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: " + rel(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: " + rel(path))
    stride = width * 4
    if len(raster) != height * (stride + 1):
        raise ValueError("decoded PNG length mismatch: " + rel(path))
    decoded = bytearray(height * stride)
    previous = bytearray(stride)
    position = 0
    filters = set()
    for y in range(height):
        filter_type = raster[position]
        position += 1
        filters.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}: {rel(path)}")
            row[i] = value & 0xFF
        decoded[y * stride:(y + 1) * stride] = row
        previous = row
    return width, height, colour_type, sorted(filters), chunks, bytes(decoded)


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


def read_ppm(path):
    data = open(path, "rb").read()
    if not data.startswith(b"P6\n"):
        raise ValueError("expected binary PPM: " + rel(path))
    parts = data.split(b"\n", 3)
    width, height = map(int, parts[1].split())
    if int(parts[2]) != 255:
        raise ValueError("expected 8-bit PPM: " + rel(path))
    rgb = parts[3]
    if len(rgb) != width * height * 3:
        raise ValueError("PPM payload length mismatch: " + rel(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 read_jpeg(path):
    data = open(path, "rb").read()
    if data[:2] != b"\xff\xd8":
        raise ValueError("not a JPEG: " + rel(path))
    offset, width, height = 2, None, 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: " + rel(path))
    return {"width": width, "height": height, "sof_marker_found": True,
            "has_eoi": data.endswith(b"\xff\xd9"), "file_bytes_read": len(data)}


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_rgb_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 digest(path):
    return hashlib.sha256(open(path, "rb").read()).hexdigest()


def negative_controls(src, spec, target_width, target_height):
    """Prove the CRC check and the pixel comparison can fail when they should."""
    results = {}
    pixels, geometry = fill_export(src, spec, target_width, target_height)
    path = os.path.join(HERE, "negative", "corrupt-crc.png")
    write_png(path, pixels, target_width, target_height)
    data = bytearray(open(path, "rb").read())
    data[60] ^= 0xFF
    open(path, "wb").write(bytes(data))
    try:
        read_png(path)
        detected, message = False, "no error raised"
    except ValueError as error:
        detected, message = True, str(error)
    results["corrupt_png_crc"] = {
        "file": "negative/corrupt-crc.png",
        "action": "flip one byte inside the IDAT chunk of a valid export",
        "detected": detected,
        "error": message,
        "error_type": "ValueError",
        "expects": "bad PNG chunk CRC",
    }
    if not detected or "CRC" not in message:
        raise ValueError("corrupt PNG was not caught by the CRC check")

    shifted, shifted_geometry = fill_export(src, spec, target_width, target_height, shift=0.001)
    differing = sum(
        bytes(pixels[y][x * 4:x * 4 + 4]) != bytes(shifted[y][x * 4:x * 4 + 4])
        for y in range(target_height)
        for x in range(target_width)
    )
    path = os.path.join(HERE, "negative", "shifted-crop.png")
    write_png(path, shifted, target_width, target_height)
    results["shifted_crop"] = {
        "file": "negative/shifted-crop.png",
        "action": "move the Fill crop down by 0.001 of the source height before sampling",
        "detected": differing > 0,
        "differing_pixels": differing,
        "compared_pixels": target_width * target_height,
        "source_y_correct": geometry["applied_source_y"],
        "source_y_shifted": shifted_geometry["applied_source_y"],
        "expects": "pixel comparison reports a non-zero difference",
    }
    if not differing:
        raise ValueError("shifted crop was not caught by the pixel comparison")
    return results


CANVAS_W = CANVAS_H = 0


def main():
    global CANVAS_W, CANVAS_H
    for directory in ("sources", "exports", "previews", "negative"):
        os.makedirs(os.path.join(HERE, directory), exist_ok=True)

    payload = {
        "generator": "make_samples.py",
        "jpeg_encoder": "cjpeg -quality 92",
        "jpeg_decoder": "djpeg",
        "jpeg_background": "RGBA composited onto #ffffff before encoding, because JPG has no alpha channel",
        "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; the browser uses its own resampler",
        "sources": [],
        "canvases": [dict(canvas) for canvas in CANVASES],
        "label_units": {
            "source_mask": "the declared 14-row band in each source; 500 pure-white source pixels per source",
            "label_rows_in_export": "raster rows in the exported canvas that hold a mask pixel (output rows, not source rows)",
            "preview.label_rows": "raster rows in the 320-pixel-wide preview that still hold a mask pixel",
            "preview.label_mask_pixels": "mask-sampled white pixels left in that 320-pixel preview",
        },
        "exports": [],
        "negative_controls": [],
        "png_readbacks": [],
    }

    built = []
    for spec in SOURCES:
        src = source_image(spec)
        mask = small_label_mask(src, spec)
        xs = [point[0] for point in mask]
        ys = [point[1] for point in mask]
        path = os.path.join(HERE, spec["file"])
        write_png(path, src, spec["width"], spec["height"])
        info = read_png(path, src)
        payload["sources"].append({
            "id": spec["id"],
            "file": spec["file"],
            "note": spec["note"],
            "width": spec["width"],
            "height": spec["height"],
            "ratio": f"{spec['width']}:{spec['height']}",
            "bytes": os.path.getsize(path),
            "sha256": digest(path),
            "png_readback": info,
            "small_label": {
                "text": LABEL,
                "source_band_y": list(LABEL_BAND[spec["id"]]),
                "source_mask_white_pixels": len(mask),
                "source_mask_bbox_xyxy": [min(xs), min(ys), max(xs), max(ys)],
            },
        })
        payload["png_readbacks"].append({"file": spec["file"], "role": "source-" + spec["id"], **info})
        built.append((spec, src, mask))

    for spec, src, mask in built:
        for target in CANVASES:
            CANVAS_W, CANVAS_H = target["width"], target["height"]
            for mode, build in (("fill", fill_export), ("fit", fit_export)):
                pixels, geometry = build(src, spec, CANVAS_W, CANVAS_H)
                stem = f"{spec['id']}-{CANVAS_W}x{CANVAS_H}-{mode}"
                png_path = os.path.join(HERE, "exports", stem + ".png")
                write_png(png_path, pixels, CANVAS_W, CANVAS_H)
                png_info = read_png(png_path, pixels)

                ppm_path = os.path.join(HERE, "exports", stem + ".ppm")
                jpg_path = os.path.join(HERE, "exports", stem + ".jpg")
                write_ppm(ppm_path, pixels, CANVAS_W, CANVAS_H)
                subprocess.run(["cjpeg", "-quality", "92", "-outfile", jpg_path, ppm_path], check=True)
                os.remove(ppm_path)
                jpeg_decode = decode_jpeg(jpg_path)

                preview_pixels, preview_h = preview(pixels, CANVAS_W, CANVAS_H, 320)
                preview_path = os.path.join(HERE, "previews", stem + "-320.png")
                write_png(preview_path, preview_pixels, 320, preview_h)
                preview_info = read_png(preview_path, preview_pixels)

                counts, fragments = marker_stats(pixels, CANVAS_W, CANVAS_H)
                row = {
                    "source": spec["id"],
                    "canvas": target["id"],
                    "width": CANVAS_W,
                    "height": CANVAS_H,
                    **geometry,
                    "png": {
                        "file": f"exports/{stem}.png",
                        "bytes": os.path.getsize(png_path),
                        "sha256": digest(png_path),
                        "readback": png_info,
                    },
                    "jpg": {
                        "file": f"exports/{stem}.jpg",
                        "bytes": os.path.getsize(jpg_path),
                        "sha256": digest(jpg_path),
                        "readback": read_jpeg(jpg_path),
                        "decode": jpeg_decode,
                    },
                    "preview": {
                        "file": f"previews/{stem}-320.png",
                        "width": 320,
                        "height": preview_h,
                        "bytes": os.path.getsize(preview_path),
                        "sha256": digest(preview_path),
                        "readback": preview_info,
                        **preview_label_stats(preview_pixels, 320, preview_h, CANVAS_W, CANVAS_H, spec, geometry, mask),
                    },
                    "label_rows_in_export": label_rows(pixels, CANVAS_H, CANVAS_W, spec, geometry, mask),
                    "marker_pixels_by_marker": counts,
                    "marker_fragments_visible": fragments,
                    "marker_pixels_total": sum(counts),
                    "transparent_pixels_png": sum(pixels[y][x * 4 + 3] == 0 for y in range(CANVAS_H) for x in range(CANVAS_W)),
                }
                payload["exports"].append(row)
                payload["png_readbacks"].append({"file": f"exports/{stem}.png", "role": stem, **png_info})

    CANVAS_W, CANVAS_H = CANVASES[0]["width"], CANVASES[0]["height"]
    payload["negative_controls"] = negative_controls(built[0][1], built[0][0], CANVAS_W, CANVAS_H)

    with open(os.path.join(HERE, "results.json"), "w", encoding="utf-8") as stream:
        json.dump(payload, stream, indent=2)
        stream.write("\n")
    print(f"wrote {len(payload['exports'])} export pairs, {len(payload['exports'])} previews, 2 sources and results.json")


if __name__ == "__main__":
    main()
