← Commit history

v0.9.110: pure-stdlib PNG encoder for screenshots - ab's managed runtime ships no PIL, so fresh boxes captured only .bmp and kicad_state returned zero images (ConfRoomROG)

John Lauer ·305f8fd56e ·2mo ago ·parent cafc00d
6 files changed +78−9
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.109+0.9.110
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.109",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.109/adom-bridge-kicad-v0.9.109.zip",-  "sha256": "1425028bec0a028a734e452dcd549cebac86b48ab024a62524e15704f75c32dc",-  "size": 406095,+  "version": "0.9.110",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.110/adom-bridge-kicad-v0.9.110.zip",+  "sha256": "61601524d3d1052914b6e00e7fe22378cb63b015eb3bb496f8e0e722f1059465",+  "size": 407549,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-15T12:30:08Z",+  "released_at": "2026-08-16T00:48:42Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.110.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.109",+  "version": "0.9.110",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/kicad_ui.py+10−2
@@ -334,8 +334,16 @@ def _screenshot_hwnd(hwnd: int, label: str = "") -> dict:         path = os.path.join(SCREENSHOT_DIR, f"kicad{suffix}-{timestamp}.png")         img.save(path, "PNG", optimize=True)     except ImportError:-        path = os.path.join(SCREENSHOT_DIR, f"kicad{suffix}-{timestamp}.bmp")-        _write_bmp(path, width, height, pixel_buf.raw)+        # ab's managed runtime is stdlib-only (no PIL) — encode PNG ourselves so+        # fresh boxes still capture (ConfRoomROG, 2026-08-15). BMP only if even+        # that fails.+        try:+            from handlers.png_stdlib import write_png_from_bgra+            path = os.path.join(SCREENSHOT_DIR, f"kicad{suffix}-{timestamp}.png")+            write_png_from_bgra(path, width, height, pixel_buf.raw, max_dim=1568)+        except Exception:+            path = os.path.join(SCREENSHOT_DIR, f"kicad{suffix}-{timestamp}.bmp")+            _write_bmp(path, width, height, pixel_buf.raw)      size_kb = os.path.getsize(path) / 1024     posix = path.replace("\\", "/")
handlers/png_stdlib.pyadded+61
@@ -0,0 +1,61 @@+"""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 ConfRoomROG, 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