implements page-numbers-plan

This commit is contained in:
rehlert committed 2026-10-28 09:25:15 +01:00
1 parent 39da8dac4d
commit b5279434b2
19 files changed
+1823 -339

No files matched your search

+100 -9
View File
@@ -11,7 +11,27 @@ export type PdfDocumentHandle = {
destroy: () => void;
};
export async function loadPdfDocument(data: Uint8Array): Promise<PdfDocumentHandle> {
/** 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<PdfPagePreview>;
destroy: () => void;
};
const loadPdfJs = async () => {
const pdfjsLib = await import('pdfjs-dist');
const { getDocument, GlobalWorkerOptions } = pdfjsLib;
@@ -19,6 +39,20 @@ export async function loadPdfDocument(data: Uint8Array): Promise<PdfDocumentHand
(module) => module.default
);
return getDocument;
};
const canvasToPngBlob = (canvas: HTMLCanvasElement) =>
new Promise<Blob>((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<PdfDocumentHandle> {
const getDocument = await loadPdfJs();
const loadingTask = getDocument({ data: new Uint8Array(data) });
const thumbnails: PdfThumbnail[] = [];
@@ -36,17 +70,10 @@ export async function loadPdfDocument(data: Uint8Array): Promise<PdfDocumentHand
if (!canvasContext) throw new Error('Could not create thumbnail canvas context.');
await page.render({ canvas, canvasContext, viewport }).promise;
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(result) =>
result ? resolve(result) : reject(new Error('Could not encode PDF thumbnail.')),
'image/png'
);
});
thumbnails.push({
pageNumber,
imageUrl: URL.createObjectURL(blob),
imageUrl: URL.createObjectURL(await canvasToPngBlob(canvas)),
width: viewport.width,
height: viewport.height
});
@@ -69,3 +96,67 @@ export async function loadPdfDocument(data: Uint8Array): Promise<PdfDocumentHand
throw error;
}
}
export async function loadPdfPageRenderer(data: Uint8Array): Promise<PdfPageRenderer> {
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();
}
};
}