export type PdfThumbnail = { pageNumber: number; imageUrl: string; width: number; height: number; }; export type PdfDocumentHandle = { pageCount: number; thumbnails: PdfThumbnail[]; destroy: () => void; }; /** Pixel scale used for on-demand page previews. */ export const PDF_PREVIEW_SCALE = 1.25; export type PdfPagePreview = { pageNumber: number; imageUrl: string; width: number; height: number; }; /** * Renders one page at a time. Used by tools that operate on a whole document * (pagination) where rasterising every page up front would exhaust memory. */ export type PdfPageRenderer = { pageCount: number; render: (pageNumber: number) => Promise; destroy: () => void; }; const loadPdfJs = async () => { const pdfjsLib = await import('pdfjs-dist'); const { getDocument, GlobalWorkerOptions } = pdfjsLib; GlobalWorkerOptions.workerSrc = await import('pdfjs-dist/build/pdf.worker.mjs?url').then( (module) => module.default ); return getDocument; }; const canvasToPngBlob = (canvas: HTMLCanvasElement) => new Promise((resolve, reject) => { canvas.toBlob( (result) => (result ? resolve(result) : reject(new Error('Could not encode PDF thumbnail.'))), 'image/png' ); }); export async function loadPdfDocument(data: Uint8Array): Promise { const getDocument = await loadPdfJs(); const loadingTask = getDocument({ data: new Uint8Array(data) }); const thumbnails: PdfThumbnail[] = []; try { const pdfDocument = await loadingTask.promise; for (let pageNumber = 1; pageNumber <= pdfDocument.numPages; pageNumber += 1) { const page = await pdfDocument.getPage(pageNumber); const viewport = page.getViewport({ scale: 0.5 }); const canvas = document.createElement('canvas'); canvas.width = Math.ceil(viewport.width); canvas.height = Math.ceil(viewport.height); const canvasContext = canvas.getContext('2d'); if (!canvasContext) throw new Error('Could not create thumbnail canvas context.'); await page.render({ canvas, canvasContext, viewport }).promise; thumbnails.push({ pageNumber, imageUrl: URL.createObjectURL(await canvasToPngBlob(canvas)), width: viewport.width, height: viewport.height }); } let destroyed = false; return { pageCount: pdfDocument.numPages, thumbnails, destroy: () => { if (destroyed) return; destroyed = true; for (const thumbnail of thumbnails) URL.revokeObjectURL(thumbnail.imageUrl); void loadingTask.destroy(); } }; } catch (error) { for (const thumbnail of thumbnails) URL.revokeObjectURL(thumbnail.imageUrl); await loadingTask.destroy(); throw error; } } export async function loadPdfPageRenderer(data: Uint8Array): Promise { const getDocument = await loadPdfJs(); const loadingTask = getDocument({ data: new Uint8Array(data) }); const pdfDocument = await loadingTask.promise; let destroyed = false; let renderToken = 0; let currentUrl: string | null = null; const releaseUrl = () => { if (currentUrl) URL.revokeObjectURL(currentUrl); currentUrl = null; }; return { pageCount: pdfDocument.numPages, render: async (pageNumber) => { if (destroyed) throw new Error('Die PDF-Vorschau wurde bereits geschlossen.'); const token = ++renderToken; const clampedPage = Math.min( pdfDocument.numPages, Math.max(1, Math.round(Number(pageNumber) || 1)) ); const page = await pdfDocument.getPage(clampedPage); const viewport = page.getViewport({ scale: PDF_PREVIEW_SCALE }); const canvas = document.createElement('canvas'); canvas.width = Math.ceil(viewport.width); canvas.height = Math.ceil(viewport.height); const canvasContext = canvas.getContext('2d'); if (!canvasContext) throw new Error('Could not create preview canvas context.'); await page.render({ canvas, canvasContext, viewport }).promise; const imageUrl = URL.createObjectURL(await canvasToPngBlob(canvas)); // A newer render (or unmount) superseded this one: drop the bitmap // instead of leaving an orphaned object URL behind. if (destroyed || token !== renderToken) { URL.revokeObjectURL(imageUrl); throw new Error('Die PDF-Vorschau wurde zwischenzeitlich geƤndert.'); } releaseUrl(); currentUrl = imageUrl; return { pageNumber: clampedPage, imageUrl, width: viewport.width, height: viewport.height }; }, destroy: () => { if (destroyed) return; destroyed = true; renderToken += 1; releaseUrl(); void loadingTask.destroy(); } }; }