main
John Lauer 0.2.21 source: script-error auto-recovery + errorShot, symbol_prims/set_models fix for AD 26.9.1, ENGINE_HALTING releas… a577bb1 1mo ago
"""png_stdlib.py — write a PNG from raw GDI BGRA pixels with ZERO dependencies.

Why: ab's managed Python runtime (adom-runtimes/python-3.12.13, ab 2.0.44+) is
stdlib-only — no PIL. On a fresh user box every screenshot fell back to .bmp,
which kicad_state refuses ("capture failed (non-png fallback?)"), so a brand-new
install had NO working captures (found on a second test machine, 2026-08-15). The bridge's
own dependency policy is stdlib-only; the screenshot path now honors it.

Input is what GetDIBits hands us: 32bpp BGRA, bottom-up. Output is an 8-bit RGB
PNG, optionally nearest-neighbor downscaled to max_dim. Channel swizzling uses
sliced bytearray assignment (C speed); only downscale column-sampling loops in
Python, over at most max_dim ints per row.
"""
import struct
import zlib
from array import array


def _chunk(tag: bytes, data: bytes) -> bytes:
    return (struct.pack(">I", len(data)) + tag + data
            + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))


def write_png_from_bgra(path: str, width: int, height: int, bgra_bottomup: bytes,
                        max_dim: int = 1568) -> tuple:
    """Write an RGB PNG; returns (out_width, out_height)."""
    if max(width, height) > max_dim:
        ratio = max_dim / max(width, height)
        nw = max(1, int(width * ratio))
        nh = max(1, int(height * ratio))
        xs = [int(x * width / nw) for x in range(nw)]
        ys = [int(y * height / nh) for y in range(nh)]
    else:
        nw, nh = width, height
        xs = None
        ys = list(range(height))

    stride = width * 4
    mv = memoryview(bgra_bottomup)
    out = bytearray()
    rgb_row = bytearray(nw * 3)
    for y in ys:
        src_y = height - 1 - y  # GDI rows are bottom-up
        row = mv[src_y * stride: src_y * stride + stride]
        if xs is not None:
            ints = row.cast("I")
            row = array("I", [ints[x] for x in xs]).tobytes()
        rgb_row[0::3] = row[2::4]  # R
        rgb_row[1::3] = row[1::4]  # G
        rgb_row[2::3] = row[0::4]  # B
        out.append(0)  # filter: none
        out += rgb_row

    ihdr = struct.pack(">IIBBBBB", nw, nh, 8, 2, 0, 0, 0)
    png = (b"\x89PNG\r\n\x1a\n"
           + _chunk(b"IHDR", ihdr)
           + _chunk(b"IDAT", zlib.compress(bytes(out), 6))
           + _chunk(b"IEND", b""))
    with open(path, "wb") as f:
        f.write(png)
    return nw, nh