91 lines
2.9 KiB
JavaScript
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); }
|
||
|
|
}
|