app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← 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)
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