e32372210412

fix Codex PDF import — crop actual code regions instead of even canvas/6 slices; shrink detached window from 460x720 to 340x560 (v3.6.74)

time
2026-05-21 02:40 UTC · 5 months ago
machine
koh2
parent
0ba54a112996
7 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/codex/pdf-import.ts+65 −24src/popup/codex/pdf-layout.ts+39 −0src/popup/codex/pdf.ts+15 −17src/popup/views/CodexRestoreView.tsx+8 −2
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.73",5+  "version": "3.6.74",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.73",4+  "version": "3.6.74",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.73",3+  "version": "3.6.74",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/codex/pdf-import.ts+65 −24
@@ -24,6 +24,16 @@2424   type ChunkEnvelope,2525 } from "../../shared/codex-types";2626 import { verifyChunkIntegrity } from "./helpers";27+import {28+  CODE_SIZE,29+  COL_GAP,30+  HEADER_GAP,31+  HEADER_HEIGHT,32+  MARGIN,33+  PAGE_HEIGHT,34+  PAGE_WIDTH,35+  ROW_STEP,36+} from "./pdf-layout";2737 2838 /**2939  * Lazily-loaded pdf.js module. Top-level `import * as pdfjsLib` would fail at@@ -150,13 +160,21 @@150160 }151161 152162 /**153- * Slice a rendered page canvas into the printed 6×6 grid and try to decode154- * each cell as a single Aztec code. Cells that contain no code (the trailing155- * empty cells on the last page) just throw NotFoundException and are skipped.163+ * Crop each of the 36 (6×6) printed Aztec codes out of the rendered page and164+ * try to decode it. Cells that contain no code — the trailing empty cells on165+ * the last page — just throw NotFoundException and are skipped.156166  *157- * Grid scanning beats whole-page scanning because zxing's binarizer locks158- * onto one symbol per call; with 36 symbols on the page it would otherwise159- * pick one and ignore the rest. We pay one decode attempt per cell — fast.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.174+ *175+ * 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.160178  */161179 async function scanCanvasGrid(162180   canvas: HTMLCanvasElement,@@ -164,28 +182,53 @@164182   seenIndices: Set<number>,165183 ): Promise<ChunkEnvelope[]> {166184   const found: ChunkEnvelope[] = [];167-  const cellW = canvas.width / CODES_PER_ROW;168-  const cellH = canvas.height / CODES_PER_COL;169185 186+  // The page was rendered at a uniform scale, so canvas pixels per PDF point187+  // is just canvas.width / PAGE_WIDTH (≈ canvas.height / PAGE_HEIGHT for an188+  // A4 source). Independent x/y scales keep us correct if the renderer ever189+  // applies non-uniform scaling.190+  const scaleX = canvas.width / PAGE_WIDTH;191+  const scaleY = canvas.height / PAGE_HEIGHT;192+193+  // Top edge of row 0 in PDF coords (bottom-origin) is just below the header.194+  // Convert to canvas coords (top-origin): canvas_y = (PAGE_HEIGHT - pdf_y) * scaleY.195+  const gridTopPdf = PAGE_HEIGHT - MARGIN - HEADER_HEIGHT - HEADER_GAP;196+  const gridTopCanvas = (PAGE_HEIGHT - gridTopPdf) * scaleY;197+198+  const codeWpx = CODE_SIZE * scaleX;199+  const codeHpx = CODE_SIZE * scaleY;200+  const quietX = Math.round(codeWpx * 0.12);201+  const quietY = Math.round(codeHpx * 0.12);202+170203   for (let row = 0; row < CODES_PER_COL; row++) {171204     for (let col = 0; col < CODES_PER_ROW; col++) {205+      const codeX = (MARGIN + col * (CODE_SIZE + COL_GAP)) * scaleX;206+      const codeY = gridTopCanvas + row * ROW_STEP * scaleY;207+208+      const sx = Math.max(0, Math.floor(codeX - quietX));209+      const sy = Math.max(0, Math.floor(codeY - quietY));210+      const sw = Math.min(211+        canvas.width - sx,212+        Math.ceil(codeWpx + 2 * quietX),213+      );214+      const sh = Math.min(215+        canvas.height - sy,216+        Math.ceil(codeHpx + 2 * quietY),217+      );218+      if (sw <= 0 || sh <= 0) continue;219+172220       const cellCanvas = document.createElement("canvas");173-      cellCanvas.width = Math.floor(cellW);174-      cellCanvas.height = Math.floor(cellH);221+      cellCanvas.width = sw;222+      cellCanvas.height = sh;175223       const cellCtx = cellCanvas.getContext("2d");176224       if (!cellCtx) continue;177225 178-      cellCtx.drawImage(179-        canvas,180-        col * cellW,181-        row * cellH,182-        cellW,183-        cellH,184-        0,185-        0,186-        cellCanvas.width,187-        cellCanvas.height,188-      );226+      // Paint the quiet-zone background white before drawing — the source227+      // PDF's cream is light but not pure white, and a clean white border228+      // gives ZXing the cleanest finder-pattern contrast.229+      cellCtx.fillStyle = "white";230+      cellCtx.fillRect(0, 0, sw, sh);231+      cellCtx.drawImage(canvas, sx, sy, sw, sh, 0, 0, sw, sh);189232 190233       let text: string;191234       try {@@ -196,10 +239,8 @@196239         const result = reader.decode(cellCanvas as unknown as HTMLImageElement);197240         text = result.getText();198241       } catch (err) {199-        // NotFoundException is normal — the trailing cells on the last page200-        // contain no code. Re-throw anything else to surface real failures.201242         if (err instanceof NotFoundException) continue;202-        continue; // any other decode failure: skip this cell243+        continue;203244       }204245 205246       const envelope = parseChunkEnvelope(text);

view file

src/popup/codex/pdf-layout.ts+39 −0
@@ -0,0 +1,39 @@1+/**2+ * pdf-layout.ts — A4 Codex page geometry shared between the generator and3+ * the PDF importer.4+ *5+ * The generator (`pdf.ts`) draws Aztec codes at exact pt positions on the6+ * page. The importer (`pdf-import.ts`) must crop the matching regions out7+ * of the rendered canvas before handing them to ZXing — Aztec decoding8+ * cannot tolerate a clipped finder pattern. Keeping the constants in one9+ * place is what stops the two from drifting (the original drift produced10+ * the "no Codex codes found" PDF-import regression).11+ *12+ * Units are PDF points (1pt = 1/72 inch). A4 = 595.28 × 841.89 pt.13+ */14+15+import { CODES_PER_ROW } from "../../shared/codex-types";16+17+export const PAGE_WIDTH = 595.28;18+export const PAGE_HEIGHT = 841.89;19+20+export const MARGIN = 28;21+22+export const HEADER_HEIGHT = 44;23+/** Vertical gap between the header hairline and the top of row 0. */24+export const HEADER_GAP = 16;25+26+export const CODE_SIZE = 80;27+28+/** Horizontal gap between adjacent codes in a row. */29+export const COL_GAP =30+  (PAGE_WIDTH - 2 * MARGIN - CODES_PER_ROW * CODE_SIZE) / (CODES_PER_ROW - 1);31+32+/** Space reserved for the per-code numeric label drawn under each Aztec. */33+export const LABEL_HEIGHT = 14;34+35+/** Vertical gap between rows (the printed white space between label and the next code). */36+export const ROW_GAP = 18;37+38+/** Top-of-code-N to top-of-code-N+1 distance — what the importer steps by between rows. */39+export const ROW_STEP = CODE_SIZE + ROW_GAP + LABEL_HEIGHT;

view file

src/popup/codex/pdf.ts+15 −17
@@ -24,26 +24,24 @@2424   CodexError,2525   type CodexManifest,2626 } from "../../shared/codex-types";27+import {28+  CODE_SIZE,29+  COL_GAP,30+  HEADER_GAP,31+  HEADER_HEIGHT,32+  MARGIN,33+  PAGE_HEIGHT,34+  PAGE_WIDTH,35+  ROW_GAP,36+} from "./pdf-layout";2737 28-// ── Layout constants (A4 in points; 1pt = 1/72 inch) ────────────────────────38+// ── Layout constants ────────────────────────────────────────────────────────39+// Page geometry (A4, margins, header height, code size, gaps) is imported40+// from ./pdf-layout so the PDF importer can crop matching regions out of the41+// rendered canvas without drifting from the generator.2942 30-const PAGE_WIDTH = 595.28;31-const PAGE_HEIGHT = 841.89;32-const MARGIN = 28;33-34-const HEADER_HEIGHT = 44;3543 const HEADER_HAIRLINE = 0.5;3644 37-// 6×6 = 36 codes per page. CODE_SIZE held at 80pt to preserve the proven38-// ~0.24mm-per-module print density; the 6-row stack clears the footer with39-// ~42pt of breathing room below the last label. Shrinking CODE_SIZE to gain40-// page-count would erode print→scan reliability — explicitly not the41-// tradeoff this surface should make.42-const CODE_SIZE = 80;43-const COL_GAP =44-  (PAGE_WIDTH - 2 * MARGIN - CODES_PER_ROW * CODE_SIZE) / (CODES_PER_ROW - 1);45-const ROW_GAP = 18;46-4745 const FOOTER_HEIGHT = 32;4846 const FOOTER_HAIRLINE = 0.5;4947 @@ -88,7 +86,7 @@8886     drawBorder(page);8987     drawHeader(page, manifest, pageIndex, pageCount, serif, mono, monoBold);9088 91-    const gridTop = PAGE_HEIGHT - MARGIN - HEADER_HEIGHT - 16;89+    const gridTop = PAGE_HEIGHT - MARGIN - HEADER_HEIGHT - HEADER_GAP;9290     drawAztecGrid(page, pageImages, startIndex, gridTop, mono);9391 9492     drawFooter(page, manifest, serif, mono);

view file

src/popup/views/CodexRestoreView.tsx+8 −2
@@ -189,11 +189,17 @@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.192198       await chrome.windows.create({193199         url: chrome.runtime.getURL(`${popupHtml}?codex=pdf`),194200         type: "popup",195-        width: 460,196-        height: 720,201+        width: 340,202+        height: 560,197203       });198204       window.close();199205     } catch (err) {

view file