12345678910111213141516171819import { BrowserAztecCodeReader, NotFoundException } from "@zxing/library";2021import {22 CODES_PER_COL,23 CODES_PER_ROW,24 type ChunkEnvelope,25} from "../../shared/codex-types";26import { verifyChunkIntegrity } from "./helpers";2728293031323334type PdfJsModule = typeof import("pdfjs-dist");35let pdfJsPromise: Promise<PdfJsModule> | null = null;3637async function getPdfJs(): Promise<PdfJsModule> {38 if (!pdfJsPromise) {39 pdfJsPromise = import("pdfjs-dist").then((mod) => {40 if (41 typeof mod.GlobalWorkerOptions.workerSrc !== "string" ||42 mod.GlobalWorkerOptions.workerSrc.length === 043 ) {44 if (typeof chrome !== "undefined" && chrome.runtime?.getURL) {45 mod.GlobalWorkerOptions.workerSrc = chrome.runtime.getURL(46 "pdf.worker.min.mjs",47 );48 } else {49 mod.GlobalWorkerOptions.workerSrc = "./pdf.worker.min.mjs";50 }51 }52 return mod;53 });54 }55 return pdfJsPromise;56}5758export interface PDFImportResult {59 60 chunksFound: number;61 62 totalExpected: number | null;63 64 envelopes: ChunkEnvelope[];65 66 errors: string[];67}6869export interface PDFImportProgress {70 (page: number, totalPages: number): void;71}7273747576777879export async function importCodexPDF(80 file: File,81 onProgress?: PDFImportProgress,82): Promise<PDFImportResult> {83 const pdfjsLib = await getPdfJs();8485 const envelopes: ChunkEnvelope[] = [];86 const errors: string[] = [];87 const seenIndices = new Set<number>();88 let totalExpected: number | null = null;8990 let pdf: Awaited<ReturnType<typeof pdfjsLib.getDocument>["promise"]>;91 try {92 const arrayBuffer = await file.arrayBuffer();93 pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;94 } catch (err) {95 return {96 chunksFound: 0,97 totalExpected: null,98 envelopes,99 errors: [100 `Could not open PDF: ${err instanceof Error ? err.message : String(err)}`,101 ],102 };103 }104105 const reader = new BrowserAztecCodeReader();106 const pageCount = pdf.numPages;107108 for (let pageNum = 1; pageNum <= pageCount; pageNum++) {109 onProgress?.(pageNum, pageCount);110111 try {112 const page = await pdf.getPage(pageNum);113 const viewport = page.getViewport({ scale: 2.0 });114115 const canvas = document.createElement("canvas");116 canvas.width = Math.ceil(viewport.width);117 canvas.height = Math.ceil(viewport.height);118 const ctx = canvas.getContext("2d");119 if (!ctx) {120 errors.push(`Page ${pageNum}: no canvas context`);121 continue;122 }123124 125 126 ctx.fillStyle = "white";127 ctx.fillRect(0, 0, canvas.width, canvas.height);128129 await page.render({ canvas, canvasContext: ctx, viewport }).promise;130131 const pageEnvelopes = await scanCanvasGrid(canvas, reader, seenIndices);132 for (const env of pageEnvelopes) envelopes.push(env);133134 if (totalExpected === null && pageEnvelopes.length > 0) {135 totalExpected = pageEnvelopes[0]!.t;136 }137 } catch (err) {138 errors.push(139 `Page ${pageNum} failed: ${err instanceof Error ? err.message : String(err)}`,140 );141 }142 }143144 return {145 chunksFound: envelopes.length,146 totalExpected,147 envelopes,148 errors,149 };150}151152153154155156157158159160161async function scanCanvasGrid(162 canvas: HTMLCanvasElement,163 reader: BrowserAztecCodeReader,164 seenIndices: Set<number>,165): Promise<ChunkEnvelope[]> {166 const found: ChunkEnvelope[] = [];167 const cellW = canvas.width / CODES_PER_ROW;168 const cellH = canvas.height / CODES_PER_COL;169170 for (let row = 0; row < CODES_PER_COL; row++) {171 for (let col = 0; col < CODES_PER_ROW; col++) {172 const cellCanvas = document.createElement("canvas");173 cellCanvas.width = Math.floor(cellW);174 cellCanvas.height = Math.floor(cellH);175 const cellCtx = cellCanvas.getContext("2d");176 if (!cellCtx) continue;177178 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 );189190 let text: string;191 try {192 193 194 195 196 const result = reader.decode(cellCanvas as unknown as HTMLImageElement);197 text = result.getText();198 } catch (err) {199 200 201 if (err instanceof NotFoundException) continue;202 continue; 203 }204205 const envelope = parseChunkEnvelope(text);206 if (!envelope) continue;207 if (seenIndices.has(envelope.i)) continue;208 if (!(await verifyChunkIntegrity(envelope))) continue;209210 seenIndices.add(envelope.i);211 found.push(envelope);212 }213 }214215 return found;216}217218219220221222223export function parseChunkEnvelope(raw: string): ChunkEnvelope | null {224 let parsed: unknown;225 try {226 parsed = JSON.parse(raw);227 } catch {228 return null;229 }230 if (typeof parsed !== "object" || parsed === null) return null;231 const obj = parsed as Record<string, unknown>;232 if (233 obj.v === 2 &&234 typeof obj.c === "string" &&235 typeof obj.i === "number" &&236 typeof obj.t === "number" &&237 typeof obj.p === "boolean" &&238 typeof obj.d === "string" &&239 typeof obj.h === "string"240 ) {241 return obj as unknown as ChunkEnvelope;242 }243 return null;244}