Files
Presentation-system/public/js/pdf-viewer.js
T
2026-09-13 20:23:05 +01:00

91 lines
2.9 KiB
JavaScript

// 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); }
}