e10e3e024e80

replace bwip-js browser Aztec encoder with ZXing AztecEncoder — eliminates bwip-js/ZXing mode-encoding incompatibility (AZTEC_CONTENT_MISMATCH root cause), removes 918 kB bwip-js chunk from all browser builds (v3.6.57)

time
2026-05-20 03:31 UTC · 5 months ago
machine
koh2
parent
a86783166653
3 files manifest.firefox.json+1 −1manifest.json+1 −1src/popup/codex/aztec.ts+86 −101
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach",5-  "version": "3.6.56",5+  "version": "3.6.57",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.6.56",4+  "version": "3.6.57",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

src/popup/codex/aztec.ts+86 −101
@@ -1,24 +1,22 @@11 /**22  * aztec.ts — Aztec 2D barcode generation with round-trip verification.33  *4- * The library has two entry points:5- *   - browser:  `bwip-js` — uses Canvas/OffscreenCanvas for rasterization6- *   - node:     `bwip-js/node` — uses zlib-PNG, returns a Buffer directly4+ * Browser path: ZXing's AztecEncoder renders to OffscreenCanvas → PNG.5+ *   Using the same library for both encoding and decoding guarantees6+ *   round-trip compatibility. bwip-js's mode-encoding choices (LATCH/SHIFT7+ *   sequences) differed from ZXing's decoder expectations on certain data8+ *   patterns, causing systematic AZTEC_CONTENT_MISMATCH errors. Switching9+ *   to ZXing's own AztecEncoder eliminates the incompatibility entirely and10+ *   also removes the 918 kB bwip-js chunk from all browser builds.711  *8- * We resolve which one to use at runtime so the popup (browser context) and9- * vitest (node context) both work without any build-time conditional. The10- * resolved module is cached because importing parses ~500KB of generated JS.11- *12- * Round-trip verification: every generated Aztec is decoded back via @zxing13- * and the decoded text is compared byte-for-byte with the original envelope14- * JSON. Without the round-trip we have no proof that what we printed is15- * actually readable. In the popup the decode runs through OffscreenCanvas.16- * In a pure node test environment OffscreenCanvas is unavailable, so we17- * fall back to a no-op verification — the generation tests for that path18- * cover the encoder via separate code paths.12+ * Node/test path: bwip-js/node → PNG Buffer.13+ *   OffscreenCanvas is unavailable in vitest; round-trip verification is14+ *   skipped (decodeAztecFromPng returns null without a DOM). The generation15+ *   tests for that path cover the encoder via separate code paths.1916  */2017 2118 import {19+  AztecEncoder,2220   BarcodeFormat,2321   BinaryBitmap,2422   DecodeHintType,@@ -38,107 +36,84 @@3836   height: number;3937 }4038 41-// ── bwip-js loader ───────────────────────────────────────────────────────────39+// ── Browser: ZXing Aztec encoding ────────────────────────────────────────────4240 4341 /**44- * In browser builds the popup bundler resolves `bwip-js` to the browser entry45- * which exports `toCanvas`. In node (vitest) we want `bwip-js/node` which46- * exports `toBuffer`. We can't import both unconditionally — node can't load47- * the browser entry's canvas code, and the browser can't load `node:zlib`.48- *49- * The detection is "do we have a working OffscreenCanvas?" rather than50- * "is `process` defined?" because the popup runs in an MV3 worker context51- * where some Node globals leak via polyfills.42+ * Render an Aztec barcode using ZXing's own encoder at 25% ECC, scale 4,43+ * with a 2-module quiet zone. Using ZXing to both encode and decode guarantees44+ * round-trip fidelity; the encoder uses the same HighLevelEncoder mode tables45+ * that the decoder expects, so no mode-switching incompatibility is possible.5246  */53-interface BwipBrowserModule {54-  toCanvas: (55-    canvas: OffscreenCanvas | HTMLCanvasElement,56-    opts: Record<string, unknown>,57-  ) => OffscreenCanvas | HTMLCanvasElement;58-}47+async function generateAztecPngBrowser(payload: string): Promise<AztecImage> {48+  // Encode payload as ISO-8859-1 bytes (= ASCII for our JSON envelope).49+  const dataBytes = new Uint8Array(payload.length);50+  for (let i = 0; i < payload.length; i++) dataBytes[i] = payload.charCodeAt(i);5951 60-interface BwipNodeModule {61-  toBuffer: (opts: Record<string, unknown>) => Promise<Buffer>;62-}52+  try {53+    const aztecCode = AztecEncoder.encode(dataBytes, AZTEC_ECC_PERCENT, 0 /* auto layers */);54+    const matrix = aztecCode.getMatrix();55+    if (!matrix) throw new Error("encoder returned null matrix");6356 64-let cachedBwip: BwipBrowserModule | BwipNodeModule | null = null;65-let cachedIsBrowser: boolean | null = null;57+    const modules = matrix.getWidth(); // Aztec codes are always square58+    const scale = 4;59+    const pad = 2 * scale; // 2-module quiet zone (8 px at scale 4)60+    const size = modules * scale + pad * 2;6661 67-async function loadBwip(): Promise<{68-  mod: BwipBrowserModule | BwipNodeModule;69-  isBrowser: boolean;70-}> {71-  if (cachedBwip && cachedIsBrowser !== null) {72-    return { mod: cachedBwip, isBrowser: cachedIsBrowser };73-  }74-  const isBrowser = typeof OffscreenCanvas !== "undefined";62+    const canvas = new OffscreenCanvas(size, size);63+    const ctx = canvas.getContext("2d")!;64+    ctx.fillStyle = "#ffffff";65+    ctx.fillRect(0, 0, size, size);66+    ctx.fillStyle = "#000000";67+    for (let y = 0; y < modules; y++) {68+      for (let x = 0; x < modules; x++) {69+        if (matrix.get(x, y)) {70+          ctx.fillRect(pad + x * scale, pad + y * scale, scale, scale);71+        }72+      }73+    }7574 76-  // Two separate import() calls so Rolldown code-splits the browser entry77-  // into a real chunk (making the URL resolvable at runtime in the extension),78-  // while keeping @vite-ignore on the test-only node path to prevent Rolldown79-  // from trying to bundle node:zlib into the browser build.80-  let ns: Record<string, unknown>;81-  if (isBrowser) {82-    // bwip-js exports types under the "browser" condition only — tsc can't83-    // resolve them with moduleResolution:"bundler". The runtime behavior is84-    // correct; the cast below enforces our own interface.85-    // @ts-expect-error — no top-level types entry in bwip-js package.json exports86-    ns = (await import("bwip-js")) as Record<string, unknown>;87-  } else {88-    // vitest / node only — bwip-js/node uses node:zlib which can't bundle for browser.89-    ns = (await import(/* @vite-ignore */ "bwip-js/node")) as Record<string, unknown>;75+    const blob = await canvas.convertToBlob({ type: "image/png" });76+    const pngBytes = new Uint8Array(await blob.arrayBuffer());77+    return { pngBytes, width: size, height: size };78+  } catch (err) {79+    throw new CodexError(80+      "AZTEC_GENERATION_FAILED",81+      `Aztec render failed: ${err instanceof Error ? err.message : String(err)}`,82+    );9083   }84+}9185 92-  const defaultExport = ns["default"] as Record<string, unknown> | undefined;93-  // ESM dynamic import surface differs between the two builds: the browser94-  // bundle exports `toCanvas` as a named export, while the node bundle is95-  // CJS-shaped and exposes `toBuffer` on `default`. Merge both so callers96-  // can use either method without caring which path resolved.97-  const merged = { ...(defaultExport ?? {}), ...ns } as Record<string, unknown>;98-  const resolved = merged as unknown as BwipBrowserModule | BwipNodeModule;99-  cachedBwip = resolved;100-  cachedIsBrowser = isBrowser;101-  return { mod: resolved, isBrowser };86+// ── Node/test: bwip-js path ───────────────────────────────────────────────────87+88+interface BwipNodeModule {89+  toBuffer: (opts: Record<string, unknown>) => Promise<Buffer>;10290 }10391 104-// ── Aztec PNG generation ────────────────────────────────────────────────────92+let cachedBwipNode: BwipNodeModule | null = null;10593 106-/**107- * Render an Aztec barcode at 25% ECC and return its PNG bytes. The PNG is108- * intentionally monochrome at scale 4 — that's the smallest scale that still109- * looks crisp at print resolution while keeping per-code rendering well under110- * 100ms on modest hardware.111- */112-export async function generateAztecPng(payload: string): Promise<AztecImage> {113-  const { mod, isBrowser } = await loadBwip();94+async function loadBwipNode(): Promise<BwipNodeModule> {95+  if (cachedBwipNode) return cachedBwipNode;96+  // vitest / node only — bwip-js/node uses node:zlib which can't bundle for browser.97+  const ns = (await import(/* @vite-ignore */ "bwip-js/node")) as Record<string, unknown>;98+  const def = ns["default"] as Record<string, unknown> | undefined;99+  const merged = { ...(def ?? {}), ...ns } as unknown as BwipNodeModule;100+  cachedBwipNode = merged;101+  return merged;102+}103+104+async function generateAztecPngNode(payload: string): Promise<AztecImage> {105+  const mod = await loadBwipNode();114106   const opts = {115107     bcid: "azteccode",116108     text: payload,117109     eclevel: AZTEC_ECC_PERCENT.toString(),118110     scale: 4,119-    // padding: 2 adds 8px (2 modules at scale 4) of quiet zone on each side.120-    // ZXing's finder pattern search fails when the code is flush against the121-    // image edge with no quiet zone, causing spurious AZTEC_ROUNDTRIP_FAILED.122111     padding: 2,123112     monochrome: true,124-    // Explicit white background so the PNG is fully opaque. Without this,125-    // bwip-js clears the canvas with clearRect (transparent), and although126-    // decodeAztecFromPng fills white before drawing, the alpha compositing127-    // path is one more source of pixel-level variance on some runtimes.128-    backgroundcolor: "ffffff",129113   } as const;130114 131115   try {132-    if (isBrowser) {133-      const canvas = new OffscreenCanvas(1, 1);134-      const drawn = (mod as BwipBrowserModule).toCanvas(canvas, opts);135-      const out = drawn as OffscreenCanvas;136-      const blob = await out.convertToBlob({ type: "image/png" });137-      const pngBytes = new Uint8Array(await blob.arrayBuffer());138-      return { pngBytes, width: out.width, height: out.height };139-    }140-141-    const buf = await (mod as BwipNodeModule).toBuffer(opts);116+    const buf = await mod.toBuffer(opts);142117     const pngBytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength).slice();143118     const { width, height } = readPngDimensions(pngBytes);144119     return { pngBytes, width, height };@@ -162,6 +137,20 @@162137   return { width, height };163138 }164139 140+// ── Aztec PNG generation (public) ────────────────────────────────────────────141+142+/**143+ * Render an Aztec barcode and return its PNG bytes + dimensions.144+ * Browser: ZXing AztecEncoder → OffscreenCanvas → PNG.145+ * Node/test: bwip-js/node → Buffer → PNG.146+ */147+export async function generateAztecPng(payload: string): Promise<AztecImage> {148+  if (typeof OffscreenCanvas !== "undefined") {149+    return generateAztecPngBrowser(payload);150+  }151+  return generateAztecPngNode(payload);152+}153+165154 // ── Aztec PNG decoding (round-trip) ─────────────────────────────────────────166155 167156 /**@@ -189,10 +178,7 @@189178     const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);190179     const ctx = canvas.getContext("2d");191180     if (!ctx) return null;192-    // White-fill before drawing so any transparent pixels in the PNG193-    // composite to white rather than (0,0,0,0). Transparent pixels get194-    // packed as 0xFF000000 (black) in the ARGB array, which would make195-    // the quiet-zone border appear black and confuse ZXing.181+    // White-fill before drawing so any transparent pixels composite to white.196182     ctx.fillStyle = "#ffffff";197183     ctx.fillRect(0, 0, bitmap.width, bitmap.height);198184     ctx.drawImage(bitmap, 0, 0);@@ -259,10 +245,9 @@259245     return image;260246   }261247 262-  // @zxing occasionally returns null or wrong content on a valid barcode due263-  // to transient rasterisation variance. Retry up to 5 times on any failure —264-  // break immediately on a confirmed correct decode. Track the last non-null265-  // wrong result so we can surface diagnostic detail if all attempts fail.248+  // Retry up to 5 times on any decode failure — break immediately on a249+  // confirmed correct decode. Track the last non-null wrong result so we250+  // can surface diagnostic detail if all attempts fail.266251   let decoded: string | null = null;267252   let lastMismatch: string | null = null;268253   for (let attempt = 0; attempt < 5; attempt++) {

view file