1cd43b988080
fix Codex PDF restore — decode canvas pixels directly via AztecCodeReader instead of BrowserAztecCodeReader (which silently fails on canvas elements); shrink detached window to 320x550 (v3.6.75)
- time
- 2026-05-21 14:23 UTC · 5 months ago
- machine
- koh2
- parent
- e32372210412
↓ peach-browser-1cd43b98.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/codex/pdf-import.ts+78 −30src/popup/views/CodexRestoreView.tsx+6 −8manifest.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.74",5+ "version": "3.6.75",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "3.6.74",4+ "version": "3.6.75",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.74",3+ "version": "3.6.75",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/codex/pdf-import.ts+78 −30
@@ -16,7 +16,14 @@1616 * cloud storage, this path is faster and more reliable than camera scanning.1717 */1818 19-import { BrowserAztecCodeReader, NotFoundException } from "@zxing/library";19+import {20+ AztecCodeReader,21+ BinaryBitmap,22+ GlobalHistogramBinarizer,23+ HybridBinarizer,24+ NotFoundException,25+ RGBLuminanceSource,26+} from "@zxing/library";2027 2128 import {2229 CODES_PER_COL,@@ -112,7 +119,6 @@112119 };113120 }114121 115- const reader = new BrowserAztecCodeReader();116122 const pageCount = pdf.numPages;117123 118124 for (let pageNum = 1; pageNum <= pageCount; pageNum++) {@@ -138,7 +144,7 @@138144 139145 await page.render({ canvas, canvasContext: ctx, viewport }).promise;140146 141- const pageEnvelopes = await scanCanvasGrid(canvas, reader, seenIndices);147+ const pageEnvelopes = await scanCanvasGrid(canvas, seenIndices);142148 for (const env of pageEnvelopes) envelopes.push(env);143149 144150 if (totalExpected === null && pageEnvelopes.length > 0) {@@ -164,21 +170,31 @@164170 * try to decode it. Cells that contain no code — the trailing empty cells on165171 * the last page — just throw NotFoundException and are skipped.166172 *167- * Cropping must follow the *exact* layout drawn by `pdf.ts`, not a naive168- * `canvas / 6` slice: the page has 28pt margins, a 60pt header band, and169- * fixed 80pt code cells with small gaps. The earlier even-grid slicer cut170- * every Aztec on its right edge, producing "no Codex codes found" even on a171- * pristine digital PDF (the binarizer can't find a finder pattern in a172- * clipped symbol). Geometry constants are shared with the generator via173- * `pdf-layout.ts` so the two can't drift again.173+ * Two non-obvious things this function gets right that earlier revisions did174+ * not:174175 *176+ * 1. Cropping follows the *exact* layout drawn by `pdf.ts` (margins,177+ * header band, code size, column/row gaps) instead of a naive178+ * `canvas / 6` slice. The naive slicer clipped every Aztec on its179+ * right edge — ZXing's binarizer can't find a finder pattern in a180+ * chopped symbol. Geometry constants are shared with the generator181+ * via `pdf-layout.ts` so the two can't drift again.182+ *183+ * 2. We decode the cell pixels directly through `AztecCodeReader` +184+ * `RGBLuminanceSource` rather than handing the canvas to185+ * `BrowserAztecCodeReader.decode()`. ZXing's BrowserCodeReader only186+ * knows how to read `HTMLVideoElement` and `HTMLImageElement` — when187+ * you pass it a canvas, its internal `createCaptureCanvas` falls188+ * through to no branch and creates a zero-sized capture canvas, so189+ * every decode returns `NotFoundException`. The DOM-free path here is190+ * the same one the generator uses for round-trip verification.191+ *175192 * Around each code we also include a small quiet-zone margin (≈12% of the176- * code size). The PDF prints codes against the cream background with no177- * required quiet zone; ZXing's locator is happier with one.193+ * code size). The PDF prints codes against a cream background with no194+ * required quiet zone; ZXing's locator is happier with a white one.178195 */179196 async function scanCanvasGrid(180197 canvas: HTMLCanvasElement,181- reader: BrowserAztecCodeReader,182198 seenIndices: Set<number>,183199 ): Promise<ChunkEnvelope[]> {184200 const found: ChunkEnvelope[] = [];@@ -200,6 +216,12 @@200216 const quietX = Math.round(codeWpx * 0.12);201217 const quietY = Math.round(codeHpx * 0.12);202218 219+ // One scratch canvas reused across all cells — cheaper than allocating220+ // 36 canvases per page when a Codex spans dozens of pages.221+ const cellCanvas = document.createElement("canvas");222+ const cellCtx = cellCanvas.getContext("2d", { willReadFrequently: true });223+ if (!cellCtx) return found;224+203225 for (let row = 0; row < CODES_PER_COL; row++) {204226 for (let col = 0; col < CODES_PER_ROW; col++) {205227 const codeX = (MARGIN + col * (CODE_SIZE + COL_GAP)) * scaleX;@@ -217,31 +239,17 @@217239 );218240 if (sw <= 0 || sh <= 0) continue;219241 220- const cellCanvas = document.createElement("canvas");221242 cellCanvas.width = sw;222243 cellCanvas.height = sh;223- const cellCtx = cellCanvas.getContext("2d");224- if (!cellCtx) continue;225-226244 // Paint the quiet-zone background white before drawing — the source227245 // PDF's cream is light but not pure white, and a clean white border228- // gives ZXing the cleanest finder-pattern contrast.246+ // gives the binarizer the cleanest finder-pattern contrast.229247 cellCtx.fillStyle = "white";230248 cellCtx.fillRect(0, 0, sw, sh);231249 cellCtx.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh);232250 233- let text: string;234- try {235- // @zxing/library's BrowserCodeReader.decode accepts any media element236- // at runtime — it branches on instanceof — but the TS signature only237- // lists HTMLImageElement. Cast through unknown to bypass the false238- // positive.239- const result = reader.decode(cellCanvas as unknown as HTMLImageElement);240- text = result.getText();241- } catch (err) {242- if (err instanceof NotFoundException) continue;243- continue;244- }251+ const text = decodeAztecFromCellPixels(cellCtx, sw, sh);252+ if (!text) continue;245253 246254 const envelope = parseChunkEnvelope(text);247255 if (!envelope) continue;@@ -254,6 +262,46 @@254262 }255263 256264 return found;265+}266+267+/**268+ * Read the cell canvas's pixels into an ARGB Int32Array, wrap it in a269+ * `RGBLuminanceSource`, and try to decode it as an Aztec code. Returns the270+ * decoded text on success, or `null` if no code was found.271+ *272+ * We try `HybridBinarizer` first (good for rendered images with subtle273+ * shading from anti-aliasing) and fall back to `GlobalHistogramBinarizer`274+ * (better when the image is dominated by clean black/white modules). A fresh275+ * `AztecCodeReader` is allocated per attempt — the reader caches internal276+ * state that must be reset between bitmaps.277+ */278+function decodeAztecFromCellPixels(279+ cellCtx: CanvasRenderingContext2D,280+ sw: number,281+ sh: number,282+): string | null {283+ const imageData = cellCtx.getImageData(0, 0, sw, sh);284+ const raw = imageData.data;285+ const argb = new Int32Array(sw * sh);286+ for (let i = 0, j = 0; i < raw.length; i += 4, j++) {287+ argb[j] = (0xff << 24) | (raw[i]! << 16) | (raw[i + 1]! << 8) | raw[i + 2]!;288+ }289+290+ const source = new RGBLuminanceSource(argb, sw, sh);291+292+ try {293+ const bitmap = new BinaryBitmap(new HybridBinarizer(source));294+ return new AztecCodeReader().decode(bitmap).getText();295+ } catch (err) {296+ if (!(err instanceof NotFoundException)) return null;297+ }298+299+ try {300+ const bitmap = new BinaryBitmap(new GlobalHistogramBinarizer(source));301+ return new AztecCodeReader().decode(bitmap).getText();302+ } catch {303+ return null;304+ }257305 }258306 259307 /**src/popup/views/CodexRestoreView.tsx+6 −8
@@ -189,17 +189,15 @@189189 // without hard-coding "src/popup/index.html".190190 const popupHtml =191191 chrome.runtime.getManifest().action?.default_popup ?? "src/popup/index.html";192- // Match the popup's intrinsic 320×520 body; add small allowances for193- // the OS title bar so the popup isn't clipped and so the rendered194- // window doesn't stretch a giant black background around a tiny popup195- // (the earlier 460×720 left ~150px of dead space on the right and196- // ~250px on the bottom). Chrome counts these as outer dimensions, so197- // the interior is ~popup-sized after subtracting native chrome.192+ // Match the popup's intrinsic 320×520 body. Chrome's popup-type193+ // window has near-zero side chrome on macOS, so width:320 lines up194+ // with the popup body and avoids the black band on the right that195+ // any wider window produces. Height gets ~30pt for the title bar.198196 await chrome.windows.create({199197 url: chrome.runtime.getURL(`${popupHtml}?codex=pdf`),200198 type: "popup",201- width: 340,202- height: 560,199+ width: 320,200+ height: 550,203201 });204202 window.close();205203 } catch (err) {