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
↓ peach-browser-e3237221.tar.gzatlas at this save
files changed7 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 −2manifest.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": {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",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": {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);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;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);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) {