diff --git a/deno.json b/deno.json index 068a6d3..af7412b 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,7 @@ "name": "bizmatch-qc-desktop", "version": "0.1.3", "exports": "./main.ts", + "unstable": ["raw-imports"], "imports": { "@std/assert": "jsr:@std/assert@^1", "@std/path": "jsr:@std/path@^1", diff --git a/main.ts b/main.ts index 1deab34..ef50d57 100644 --- a/main.ts +++ b/main.ts @@ -25,6 +25,147 @@ import PDFJS_LIB_TEXT from "pdfjs-dist/legacy/build/pdf.min.mjs" with { import PDFJS_WORKER_TEXT from "pdfjs-dist/legacy/build/pdf.worker.min.mjs" with { type: "text", }; +// PDF.js >= 5 needs these auxiliary assets at runtime (fetched by the worker): +// - wasm/: CCITT-G4 + JBIG2 (B/W scans), JPEG2000 and qcms (ICC) decoders +// - standard_fonts/: base-14 font substitutes for PDFs without embedded fonts +// - iccs/: default CMYK ICC profile +// Embedded via import attributes so the compiled desktop binary stays self-contained. +import WASM_JBIG2 from "pdfjs-dist/wasm/jbig2.wasm" with { type: "bytes" }; +import WASM_OPENJPEG from "pdfjs-dist/wasm/openjpeg.wasm" with { + type: "bytes", +}; +import WASM_QCMS from "pdfjs-dist/wasm/qcms_bg.wasm" with { type: "bytes" }; +import JS_JBIG2_FALLBACK from "pdfjs-dist/wasm/jbig2_nowasm_fallback.js" with { + type: "text", +}; +import JS_OPENJPEG_FALLBACK from "pdfjs-dist/wasm/openjpeg_nowasm_fallback.js" with { + type: "text", +}; +import ICC_CGATS from "pdfjs-dist/iccs/CGATS001Compat-v2-micro.icc" with { + type: "bytes", +}; +import FONT_DINGBATS from "pdfjs-dist/standard_fonts/FoxitDingbats.pfb" with { + type: "bytes", +}; +import FONT_FIXED from "pdfjs-dist/standard_fonts/FoxitFixed.pfb" with { + type: "bytes", +}; +import FONT_FIXED_B from "pdfjs-dist/standard_fonts/FoxitFixedBold.pfb" with { + type: "bytes", +}; +import FONT_FIXED_BI from "pdfjs-dist/standard_fonts/FoxitFixedBoldItalic.pfb" with { + type: "bytes", +}; +import FONT_FIXED_I from "pdfjs-dist/standard_fonts/FoxitFixedItalic.pfb" with { + type: "bytes", +}; +import FONT_SERIF from "pdfjs-dist/standard_fonts/FoxitSerif.pfb" with { + type: "bytes", +}; +import FONT_SERIF_B from "pdfjs-dist/standard_fonts/FoxitSerifBold.pfb" with { + type: "bytes", +}; +import FONT_SERIF_BI from "pdfjs-dist/standard_fonts/FoxitSerifBoldItalic.pfb" with { + type: "bytes", +}; +import FONT_SERIF_I from "pdfjs-dist/standard_fonts/FoxitSerifItalic.pfb" with { + type: "bytes", +}; +import FONT_SYMBOL from "pdfjs-dist/standard_fonts/FoxitSymbol.pfb" with { + type: "bytes", +}; +import FONT_LSANS from "pdfjs-dist/standard_fonts/LiberationSans-Regular.ttf" with { + type: "bytes", +}; +import FONT_LSANS_B from "pdfjs-dist/standard_fonts/LiberationSans-Bold.ttf" with { + type: "bytes", +}; +import FONT_LSANS_BI from "pdfjs-dist/standard_fonts/LiberationSans-BoldItalic.ttf" with { + type: "bytes", +}; +import FONT_LSANS_I from "pdfjs-dist/standard_fonts/LiberationSans-Italic.ttf" with { + type: "bytes", +}; + +const PDFJS_ASSETS: Record< + string, + { body: Uint8Array | string; type: string } +> = { + "/pdfjs/wasm/jbig2.wasm": { body: WASM_JBIG2, type: "application/wasm" }, + "/pdfjs/wasm/openjpeg.wasm": { + body: WASM_OPENJPEG, + type: "application/wasm", + }, + "/pdfjs/wasm/qcms_bg.wasm": { body: WASM_QCMS, type: "application/wasm" }, + "/pdfjs/wasm/jbig2_nowasm_fallback.js": { + body: JS_JBIG2_FALLBACK, + type: "text/javascript; charset=utf-8", + }, + "/pdfjs/wasm/openjpeg_nowasm_fallback.js": { + body: JS_OPENJPEG_FALLBACK, + type: "text/javascript; charset=utf-8", + }, + "/pdfjs/iccs/CGATS001Compat-v2-micro.icc": { + body: ICC_CGATS, + type: "application/vnd.iccprofile", + }, + "/pdfjs/standard_fonts/FoxitDingbats.pfb": { + body: FONT_DINGBATS, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitFixed.pfb": { + body: FONT_FIXED, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitFixedBold.pfb": { + body: FONT_FIXED_B, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitFixedBoldItalic.pfb": { + body: FONT_FIXED_BI, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitFixedItalic.pfb": { + body: FONT_FIXED_I, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitSerif.pfb": { + body: FONT_SERIF, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitSerifBold.pfb": { + body: FONT_SERIF_B, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitSerifBoldItalic.pfb": { + body: FONT_SERIF_BI, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitSerifItalic.pfb": { + body: FONT_SERIF_I, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/FoxitSymbol.pfb": { + body: FONT_SYMBOL, + type: "application/octet-stream", + }, + "/pdfjs/standard_fonts/LiberationSans-Regular.ttf": { + body: FONT_LSANS, + type: "font/ttf", + }, + "/pdfjs/standard_fonts/LiberationSans-Bold.ttf": { + body: FONT_LSANS_B, + type: "font/ttf", + }, + "/pdfjs/standard_fonts/LiberationSans-BoldItalic.ttf": { + body: FONT_LSANS_BI, + type: "font/ttf", + }, + "/pdfjs/standard_fonts/LiberationSans-Italic.ttf": { + body: FONT_LSANS_I, + type: "font/ttf", + }, +}; const PREFIX = "[BizMatch QC]"; @@ -452,6 +593,17 @@ Deno.serve(async (request) => { }); } + // ---- PDF.js auxiliary assets (wasm decoders, standard fonts, ICC) ---- + const pdfjsAsset = PDFJS_ASSETS[url.pathname]; + if (pdfjsAsset) { + return new Response(pdfjsAsset.body as BodyInit, { + headers: { + "content-type": pdfjsAsset.type, + "cache-control": "public, max-age=86400", + }, + }); + } + // ---- PDF.js library assets ---- if (url.pathname === "/pdfjs/legacy/pdf.min.mjs") { return new Response(PDFJS_LIB_TEXT, { @@ -613,4 +765,4 @@ try { ); } -win.show(); +win.show(); \ No newline at end of file diff --git a/web/app.js b/web/app.js index 4830f5d..ff51b4d 100644 --- a/web/app.js +++ b/web/app.js @@ -1,6 +1,7 @@ let state; let groups = []; let selectedIndex = -1; +let loadedIndex = -1; // which document the PDF viewer currently shows/loads let pdfViewer = null; let loadRequestId = 0; @@ -116,10 +117,14 @@ async function loadPdf(index) { const doc = state.documents[index]; if (!doc) return; - // Deduplicate: don't reload the same document - if (index === selectedIndex && pdfViewer) { + // Deduplicate: don't reload the document that is already shown. + // NOTE: must compare against loadedIndex, NOT selectedIndex -- + // select() updates selectedIndex before calling loadPdf(), so a + // selectedIndex comparison is always true and blocks every reload. + if (index === loadedIndex && pdfViewer) { return; } + loadedIndex = index; if (pdfViewer) { pdfViewer.destroy(); @@ -164,6 +169,7 @@ async function loadPdf(index) { console.log(`[BizMatch QC] PDF ${reqId} rendering complete`); } catch (error) { if (index !== selectedIndex) return; + loadedIndex = -1; // allow retry: clicking the same document again reloads it const message = error instanceof Error ? error.message : String(error); pdfMessage.textContent = `Cannot open PDF: ${message}`; pdfMessage.hidden = false; @@ -281,6 +287,7 @@ async function load() { throw new Error(`State request failed with HTTP ${response.status}.`); } state = await response.json(); + loadedIndex = -1; // document set changed; index-based dedup is invalid now groups = buildGroups(state.documents); showError(state.loadError || ""); renderList(); @@ -290,4 +297,4 @@ async function load() { } } -void load(); +void load(); \ No newline at end of file diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 570de0c..224f930 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -71,6 +71,13 @@ class PdfViewer { url, rangeChunkSize: 65536, disableAutoFetch: false, + // PDF.js >= 5 decodes CCITT-G4/JBIG2 (B/W scans), JPEG2000 and ICC + // color via WASM modules fetched from wasmUrl. Without these URLs + // the decoders fail silently (ignoreErrors default) and scanned + // pages render as blank white canvases of the correct size. + wasmUrl: "/pdfjs/wasm/", + standardFontDataUrl: "/pdfjs/standard_fonts/", + iccUrl: "/pdfjs/iccs/", }); this.loadingTask = task; @@ -259,4 +266,4 @@ function errorMsg(err) { return err instanceof Error ? err.message : String(err); } -globalThis.PdfViewer = PdfViewer; +globalThis.PdfViewer = PdfViewer; \ No newline at end of file