← Commit history

Classic BMP-in-ICO favicons finally decode: a new DIB-to-RGBA decoder handles the 32bpp uncompressed entries that ti.com and mouser.com ship, which analyzeIconBytes had been discarding as unreadable even though both fetch rails were delivering the correct bytes all along, so those taskbar badges now show the real red Texas Instruments logo and blue Mouser M instead of the generic globe.

John Lauer ·d3d82947e4 ·2mo ago ·parent c76db62
4 files changed +63−9
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.211",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.211/adom-bridge-puppeteer-v2.0.211.zip",-  "sha256": "9edc441655f413c1dc1edafc41f2c478d3caa457d378267f94211560e4ebfd0c",-  "size": 936111,+  "version": "2.0.212",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.212/adom-bridge-puppeteer-v2.0.212.zip",+  "sha256": "a1c96f8ccd12db5a3c74ae79f1817aeb6080bf2f0cf2f5e6fde0686a429842ec",+  "size": 936976,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.211+2.0.212
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.211",+  "version": "2.0.212",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
src/server.js+57−3
@@ -5746,6 +5746,60 @@ function icoLargestPng(buf) {   } catch (e) { return null; } } +function icoLargestDibRgba(buf) {+  // v2.0.212 (#55 root cause, at last): ti.com and mouser.com ship CLASSIC BMP-in-ICO favicons+  // (32bpp uncompressed DIB entries, zero PNG entries). icoLargestPng only handles PNG-in-ICO, so+  // analyzeIconBytes judged these real, correctly-fetched icons "unreadable" and the badge went+  // generic — the network rails were never the problem. Decode the largest 32bpp DIB to raw RGBA+  // for sharp. Rows are bottom-up BGRA; height in the info header is doubled (XOR + AND mask);+  // an all-zero alpha channel means "use the AND mask", so apply it in that case.+  try {+    if (!buf || buf.length < 22) return null;+    if (buf.readUInt16LE(0) !== 0 || buf.readUInt16LE(2) !== 1) return null;+    const n = buf.readUInt16LE(4);+    let best = null;+    for (let i = 0; i < n; i++) {+      const e = 6 + i * 16;+      if (e + 16 > buf.length) break;+      const w = buf[e] === 0 ? 256 : buf[e];+      const h = buf[e + 1] === 0 ? 256 : buf[e + 1];+      const size = buf.readUInt32LE(e + 8);+      const off = buf.readUInt32LE(e + 12);+      if (off + size > buf.length || size <= 40) continue;+      const d = buf.subarray(off, off + size);+      if (d[0] === 0x89 && d[1] === 0x50) continue;                       // PNG entry — icoLargestPng's job+      if (d.readUInt32LE(0) !== 40) continue;                             // only plain BITMAPINFOHEADER+      if (d.readUInt16LE(14) !== 32 || d.readUInt32LE(16) !== 0) continue; // only 32bpp uncompressed+      if (!best || w * h > best.w * best.h) best = { w, h, d };+    }+    if (!best) return null;+    const { w, h, d } = best;+    const stride = w * 4;+    if (40 + stride * h > d.length) return null;+    const out = Buffer.alloc(w * h * 4);+    let maxA = 0;+    for (let y = 0; y < h; y++) {+      const srow = 40 + (h - 1 - y) * stride, drow = y * stride;+      for (let x = 0; x < w; x++) {+        const si = srow + x * 4, di = drow + x * 4;+        out[di] = d[si + 2]; out[di + 1] = d[si + 1]; out[di + 2] = d[si]; out[di + 3] = d[si + 3];+        if (d[si + 3] > maxA) maxA = d[si + 3];+      }+    }+    if (maxA === 0) {                                                     // pre-alpha ICO: AND mask rules+      const maskStride = Math.ceil(w / 32) * 4, maskOff = 40 + stride * h;+      for (let y = 0; y < h; y++) {+        const mrow = maskOff + (h - 1 - y) * maskStride;+        for (let x = 0; x < w; x++) {+          const bit = mrow + (x >> 3) < d.length ? (d[mrow + (x >> 3)] >> (7 - (x & 7))) & 1 : 0;+          out[y * stride + x * 4 + 3] = bit ? 0 : 255;+        }+      }+    }+    return { rgba: out, w, h };+  } catch (e) { return null; }+}+ // v1.9.238 (John: "favicons these days can be svg or png, dark mode or light mode, transparent... you // seem to not be doing much logic there... determine which favicon to retrieve to have it show the // best/nicest in the windows taskbar"). A proper resolver that (1) enumerates EVERY declared icon with@@ -5824,18 +5878,18 @@ async function analyzeIconBytes(b64, mime, href) {   // Rasterize to PNG and measure transparency + luminance. Returns null if truly undecodable.   const buf = Buffer.from(b64, 'base64');   const ext = ((href || '').split('?')[0].split('.').pop() || '').toLowerCase();-  let format = 'png', input = buf;+  let format = 'png', input = buf, _rawOpts = null;   if (mime.includes('svg') || ext === 'svg') format = 'svg';   else if (mime.includes('png') || ext === 'png') format = 'png';   else if (mime.includes('jpeg') || ext === 'jpg' || ext === 'jpeg') format = 'jpeg';-  else if (mime.includes('icon') || mime.includes('ico') || ext === 'ico') { format = 'ico'; const inner = icoLargestPng(buf); if (inner) input = inner; }+  else if (mime.includes('icon') || mime.includes('ico') || ext === 'ico') { format = 'ico'; const inner = icoLargestPng(buf); if (inner) input = inner; else { const dib = icoLargestDibRgba(buf); if (dib) { input = dib.rgba; _rawOpts = { raw: { width: dib.w, height: dib.h, channels: 4 } }; } } }   try {     // v1.9.240: RASTERIZE at a fixed size FIRST, then measure. An SVG with only a viewBox (no     // width/height — like our own icon.svg) has no intrinsic size, so sharp.stats() on the raw bytes     // throws "unsupported image" and the icon was wrongly judged unreadable. A high render density     // keeps the raster crisp; measuring the PNG (always decodable) makes transparency/luminance robust.     const S = require('sharp');   // module-scope var is named `sharp`; other fns re-require locally — do the same-    const opts = format === 'svg' ? { density: 384 } : {};+    const opts = _rawOpts ? _rawOpts : (format === 'svg' ? { density: 384 } : {});     const pngBuf = await S(input, opts).resize(48, 48, { fit: 'inside', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer();     const stats = await S(pngBuf).stats();     const ch = stats.channels || [];