// Loads PDF.js (vendored first, CDN fallback) and renders one page at a time, // scaled to fit its container and sharp on high-DPI screens. let readyPromise = null; function loadScript(src) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = () => reject(new Error('Failed to load ' + src)); document.head.appendChild(s); }); } export function ensurePdfjs() { if (window.pdfjsLib) return Promise.resolve(); if (readyPromise) return readyPromise; const CDN = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174'; readyPromise = loadScript('/vendor/pdfjs/pdf.min.js') .then(() => { if (!window.pdfjsLib) throw new Error('vendor missing'); window.__pdfWorker = '/vendor/pdfjs/pdf.worker.min.js'; }) .catch(() => loadScript(`${CDN}/pdf.min.js`).then(() => { window.__pdfWorker = `${CDN}/pdf.worker.min.js`; })) .then(() => { window.pdfjsLib.GlobalWorkerOptions.workerSrc = window.__pdfWorker; }); return readyPromise; } export class PdfViewer { constructor(container) { this.container = container; this.canvas = document.createElement('canvas'); this.canvas.className = 'pdf-canvas'; this.ctx = this.canvas.getContext('2d'); container.appendChild(this.canvas); this.pdf = null; this.numPages = 0; this.page = 1; this._busy = false; this._queued = null; this._onResize = () => this.refit(); window.addEventListener('resize', this._onResize); } async load(url) { await ensurePdfjs(); this.pdf = await window.pdfjsLib.getDocument(url).promise; this.numPages = this.pdf.numPages; return this.numPages; } async render(n) { if (!this.pdf) return; n = Math.max(1, Math.min(this.numPages, Math.trunc(n) || 1)); this.page = n; if (this._busy) { this._queued = n; return; } this._busy = true; try { const page = await this.pdf.getPage(n); const base = page.getViewport({ scale: 1 }); const cw = this.container.clientWidth || 800; const ch = this.container.clientHeight || 600; const dpr = Math.min(window.devicePixelRatio || 1, 2); const fit = Math.min(cw / base.width, ch / base.height); const vp = page.getViewport({ scale: fit * dpr }); this.canvas.width = Math.floor(vp.width); this.canvas.height = Math.floor(vp.height); this.canvas.style.width = Math.floor(vp.width / dpr) + 'px'; this.canvas.style.height = Math.floor(vp.height / dpr) + 'px'; await page.render({ canvasContext: this.ctx, viewport: vp }).promise; } finally { this._busy = false; if (this._queued !== null) { const q = this._queued; this._queued = null; if (q !== n) this.render(q); } } } refit() { if (this.pdf) this.render(this.page); } destroy() { window.removeEventListener('resize', this._onResize); } }