← Commit history

v1.9.48: stepShot works without PIL (stdlib BMP->PNG downscaler); inline gallery capped at 4MB with downscaled fallback

John Lauer ·cac5a2ddc2 ·1mo ago ·parent 5f439f2
5 files changed +82−14
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.47\ No newline at end of file+1.9.48\ No newline at end of file
CHANGELOG.md+10
@@ -3,6 +3,16 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.9.48 - 2026-08-16++- **stepShot frames actually deliver on PIL-less bridges.** AD's provisioned python has no PIL,+  so captures fall back to raw ~24MB BMPs; 1.9.47's encoder silently skipped every one (live+  tour: 9 shots, 0 stepShot frames). New pure-stdlib BMP->PNG nearest-neighbor downscaler+  (zlib+struct, no deps) feeds the ~640px stepShot contract regardless of PIL.+- **Inline gallery hard cap.** imagesAs:base64 now refuses to inline files over 4MB and ships+  the downscaled (<=1280px) frame + imageNote instead - a BMP-era done frame would have been a+  ~280MB response.+ ## 1.9.47 - 2026-08-16  - **Live per-step tour evidence (ab's #30 contract, John's call).** fusion_demo_progress frames
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.47",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.47/adom-bridge-fusion-v1.9.47.zip",-    "sha256": "dd86103860fcbb45e3345f8fd9171bcf96184a6a49bf1a57685f85f9ffef94de",-    "size": 395774,+    "version": "1.9.48",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.48/adom-bridge-fusion-v1.9.48.zip",+    "sha256": "94fc6edeb64a8366928d736bdc574282a3d2e3448ff4d2ca5e42b6fdbb0e7fc4",+    "size": 396880,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.47",+  "version": "1.9.48",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
server.py+66−8
@@ -6216,14 +6216,62 @@ def _demo_tour_load(demo_id: str):     return None  +def _bmp_downscale_png(path: str, max_w: int = 640):+    """Pure-STDLIB BMP -> small PNG (nearest-neighbor). Exists because AD's provisioned+    python has NO PIL, so screenshot captures fall back to raw 24MB BMPs - and a 24MB BMP+    is 31MB as base64, which no relay frame should ever carry (caught live 2026-08-16:+    every tour stepShot silently skipped, and the done gallery would have been ~280MB).+    BMP is uncompressed BGR rows and PNG needs only zlib+struct, so no dependency."""+    import struct+    import zlib+    raw = open(path, "rb").read()+    if raw[:2] != b"BM":+        raise ValueError("not a BMP")+    pix_off = struct.unpack_from("<I", raw, 10)[0]+    w = struct.unpack_from("<i", raw, 18)[0]+    h = struct.unpack_from("<i", raw, 22)[0]+    bpp = struct.unpack_from("<H", raw, 28)[0]+    comp = struct.unpack_from("<I", raw, 30)[0]+    if comp != 0 or bpp not in (24, 32) or w <= 0 or h == 0:+        raise ValueError("unsupported BMP (bpp=%s comp=%s)" % (bpp, comp))+    bottom_up = h > 0+    h = abs(h)+    bypp = bpp // 8+    stride = ((w * bypp) + 3) & ~3+    ow = min(max_w, w)+    oh = max(1, int(h * ow / w))+    xs = [int(x * w / ow) * bypp for x in range(ow)]+    rows = []+    for y in range(oh):+        sy = int(y * h / oh)+        row_off = pix_off + ((h - 1 - sy) if bottom_up else sy) * stride+        line = bytearray(1 + ow * 3)  # PNG filter byte 0 + RGB+        i = 1+        for xo in xs:+            o = row_off + xo+            line[i] = raw[o + 2]      # R (BMP stores BGR)+            line[i + 1] = raw[o + 1]  # G+            line[i + 2] = raw[o]      # B+            i += 3+        rows.append(bytes(line))+    def chunk(tag, data):+        return (struct.pack(">I", len(data)) + tag + data+                + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))+    ihdr = struct.pack(">IIBBBBB", ow, oh, 8, 2, 0, 0, 0)+    idat = zlib.compress(b"".join(rows), 6)+    return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat)+            + chunk(b"IEND", b""))++ def _shot_b64_small(path: str, max_w: int = 640):     """Base64 evidence frame for ab's stepShot contract (~640px wide). PIL downscale when-    available; else the raw file if it is small enough for the relay envelope."""+    available; the stdlib BMP downscaler when not (AD's python ships without PIL); else+    the raw file if it is small enough for the relay envelope."""+    import base64     try:         try:             from PIL import Image             import io-            import base64             im = Image.open(path)             if im.width > max_w:                 im = im.resize((max_w, max(1, int(im.height * max_w / im.width))))@@ -6231,10 +6279,12 @@ def _shot_b64_small(path: str, max_w: int = 640):             im.convert("RGB").save(buf, format="PNG")             return base64.b64encode(buf.getvalue()).decode()         except ImportError:-            import base64-            raw = open(path, "rb").read()-            if len(raw) <= 1_500_000:-                return base64.b64encode(raw).decode()+            pass+        if path.lower().endswith(".bmp"):+            return base64.b64encode(_bmp_downscale_png(path, max_w)).decode()+        raw = open(path, "rb").read()+        if len(raw) <= 1_500_000:+            return base64.b64encode(raw).decode()     except Exception:         pass     return None@@ -6574,8 +6624,16 @@ def _handle_demo_progress(args: dict) -> dict:         for s in shots:             s = dict(s)             try:-                with open(s["path"], "rb") as f:-                    s["image"] = base64.b64encode(f.read()).decode()+                raw = open(s["path"], "rb").read()+                if len(raw) <= 4 * 1024 * 1024:+                    s["image"] = base64.b64encode(raw).decode()+                else:+                    # PIL-less bridge python saves raw 24MB BMPs - NEVER ship those inline+                    # (9 x 31MB base64 = a ~280MB frame). Downscaled evidence instead.+                    img = _shot_b64_small(s["path"], max_w=1280)+                    if img:+                        s["image"] = img+                        s["imageNote"] = "downscaled to <=1280px (original is a %dMB capture; pull_file the path for full size)" % (len(raw) // (1024 * 1024))             except Exception:                 pass             encoded.append(s)