// PDF.js viewer using pdfjs-dist 6.1.200 legacy build. // Depends on global pdfjsLib (loaded in index.html). class PdfViewer { constructor(container) { this.container = container; this.generation = 0; this.loadingTask = null; this.doc = null; this.renderTasks = new Set(); this.lastUrl = null; this.lastSize = null; this._createDOM(); } _createDOM() { // The pages container is always visible (never hidden). // Loading and error states are overlays on top. this.container.innerHTML = `
`; this.pagesDiv = this.container.querySelector(".pdf-v-pages"); this.loadingDiv = this.container.querySelector(".pdf-v-loading"); this.errorDiv = this.container.querySelector(".pdf-v-error"); this.errorMsg = this.container.querySelector(".pdf-v-error-msg"); this.retryBtn = this.container.querySelector(".pdf-v-retry"); this.retryBtn.addEventListener("click", () => { if (this.lastUrl != null) { this._setState("loading"); this._loadInternal(this.lastUrl, this.lastSize); } }); } _setState(state) { this.loadingDiv.hidden = true; this.errorDiv.hidden = true; if (state === "loading") this.loadingDiv.hidden = false; if (state === "error") this.errorDiv.hidden = false; this._viewerState = state; } async load(url, byteSize) { this.lastUrl = url; this.lastSize = byteSize; this._setState("loading"); await this._loadInternal(url, byteSize); } async _loadInternal(url, _byteSize) { const generation = ++this.generation; console.log( `[BizMatch QC] PDF viewer load started: generation ${generation}`, ); // Await cleanup of previous document (promises may be involved) await this._disposeCurrentDocument(); if (generation !== this.generation) return; this.pagesDiv.innerHTML = ""; const loadStart = performance.now(); try { const task = pdfjsLib.getDocument({ url, rangeChunkSize: 65536, disableAutoFetch: false, }); this.loadingTask = task; const doc = await task.promise; if (generation !== this.generation) { doc.destroy(); return; } this.doc = doc; const numPages = doc.numPages; const loadMs = Math.round(performance.now() - loadStart); console.log( `[BizMatch QC] PDF document loaded: ${numPages} pages in ${loadMs}ms`, ); const viewerWidth = this._getViewportWidth(); console.log(`[BizMatch QC] PDF usable render width: ${viewerWidth}px`); const pageStart = performance.now(); await this._renderPage(doc, 1, viewerWidth, generation); if (generation !== this.generation) return; this._setState(null); // Remove loading overlay const firstPageMs = Math.round(performance.now() - pageStart); console.log( `[BizMatch QC] PDF page 1 rendered in ${firstPageMs}ms`, ); console.log( `[BizMatch QC] First page visible after ${ Math.round(performance.now() - loadStart) }ms`, ); for (let p = 2; p <= numPages; p++) { if (generation !== this.generation) return; const pStart = performance.now(); await this._renderPage(doc, p, viewerWidth, generation); if (generation !== this.generation) return; console.log( `[BizMatch QC] PDF page ${p} rendered in ${ Math.round(performance.now() - pStart) }ms`, ); await new Promise((r) => requestAnimationFrame(r)); } const totalMs = Math.round(performance.now() - loadStart); console.log( `[BizMatch QC] PDF viewer load completed: generation ${generation}`, ); console.log( `[BizMatch QC] PDF fully rendered: ${numPages} pages in ${totalMs}ms`, ); } catch (err) { if (generation !== this.generation) return; // Distinguish cancellation from real errors if (isCancellationError(err)) { console.log( `[BizMatch QC] PDF generation ${generation} cancelled`, ); return; } const msg = errorMsg(err); console.error(`[BizMatch QC] PDF load failed:`, msg); this.errorMsg.textContent = `Unable to open PDF: ${msg}`; this._setState("error"); } } _getViewportWidth() { const style = globalThis.getComputedStyle ? getComputedStyle(this.pagesDiv) : null; let padding = 24; // default guess if (style) { padding = (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0); } const raw = Math.round(this.pagesDiv.clientWidth || 600); return Math.max(raw - padding, 200); } async _disposeCurrentDocument() { for (const rt of this.renderTasks) { try { rt.cancel(); } catch { /* ok */ } } this.renderTasks.clear(); if (this.loadingTask) { try { await this.loadingTask.destroy(); } catch { /* ok */ } this.loadingTask = null; } if (this.doc) { try { await this.doc.destroy(); } catch { /* ok */ } this.doc = null; } } async _renderPage(doc, pageNum, viewerWidth, generation) { const pageDiv = document.createElement("div"); pageDiv.className = "pdf-v-page"; pageDiv.dataset.page = String(pageNum); pageDiv.dataset.renderState = "idle"; const canvas = document.createElement("canvas"); pageDiv.appendChild(canvas); this.pagesDiv.appendChild(pageDiv); pageDiv.dataset.renderState = "rendering"; try { const page = await doc.getPage(pageNum); if (generation !== this.generation) return; const vp1 = page.getViewport({ scale: 1 }); const fitScale = viewerWidth / vp1.width; const viewport = page.getViewport({ scale: fitScale }); const dpr = Math.min(globalThis.devicePixelRatio || 1, 2); canvas.width = Math.floor(viewport.width * dpr); canvas.height = Math.floor(viewport.height * dpr); canvas.style.width = `${Math.floor(viewport.width)}px`; canvas.style.height = `${Math.floor(viewport.height)}px`; const ctx = canvas.getContext("2d"); const transform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined; const renderTask = page.render({ canvasContext: ctx, viewport, transform, }); this.renderTasks.add(renderTask); await renderTask.promise; this.renderTasks.delete(renderTask); if (generation !== this.generation) return; pageDiv.dataset.renderState = "rendered"; } catch (err) { if (generation !== this.generation) return; if (isCancellationError(err)) return; pageDiv.dataset.renderState = "error"; console.error( `[BizMatch QC] Failed to render PDF page ${pageNum}`, err, ); if (pageNum === 1) { this.errorMsg.textContent = `Unable to open PDF: ${errorMsg(err)}`; this._setState("error"); } else { pageDiv.textContent = `Error rendering page ${pageNum}`; pageDiv.style.padding = "20px"; pageDiv.style.color = "#f88"; pageDiv.style.textAlign = "center"; } } } destroy() { this.generation = 0; this._disposeCurrentDocument().catch(() => {}); this.pagesDiv.innerHTML = ""; this._setState("loading"); } } function isCancellationError(err) { if (!(err instanceof Error)) return false; const msg = err.message || ""; return msg.includes("cancelled") || msg.includes("cancelled") || msg.includes("destroyed") || msg.includes("Worker was destroyed"); } function errorMsg(err) { return err instanceof Error ? err.message : String(err); } globalThis.PdfViewer = PdfViewer;