import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import sharp from "sharp"; const TARGET_DPI = 200; const PDF_DEFAULT_DPI = 72; const SCALE = TARGET_DPI / PDF_DEFAULT_DPI; export interface PageImage { page: number; buffer: Buffer; width: number; height: number; } interface CanvasAndContext { canvas: { toBuffer(mime: string): Buffer }; context: unknown; } interface CanvasFactory { create(width: number, height: number): CanvasAndContext; } export async function pdfToImages(pdfBuffer: Buffer): Promise { const data = new Uint8Array(pdfBuffer); const loadingTask = getDocument({ data, useSystemFonts: true }); const pdfDocument = await loadingTask.promise; const pageCount = pdfDocument.numPages; const images: PageImage[] = []; const canvasFactory = pdfDocument.canvasFactory as unknown as CanvasFactory; console.log(`[pdf] Rendering ${pageCount} page(s) at ${TARGET_DPI} DPI (scale ${SCALE.toFixed(2)})`); for (let i = 1; i <= pageCount; i++) { const page = await pdfDocument.getPage(i); const viewport = page.getViewport({ scale: SCALE }); const { canvas, context } = canvasFactory.create( Math.floor(viewport.width), Math.floor(viewport.height) ); // eslint-disable-next-line @typescript-eslint/no-explicit-any await page.render({ canvas: canvas as any, viewport } as any).promise; const pngBuffer = canvas.toBuffer("image/png"); page.cleanup(); let buf = await sharp(pngBuffer).jpeg({ quality: 90 }).toBuffer(); let meta = await sharp(buf).metadata(); const w = meta.width || 0; const h = meta.height || 0; // Duplex scanners flip the sheet between sides, so odd pages // (back/response card) are 180-deg rotated relative to even pages // (front/survey). Landscape pages need rotation to portrait; // the direction depends on which side of the sheet was scanned. const isBackSide = i % 2 === 1; if (w > h) { const angle = isBackSide ? -90 : 90; buf = await sharp(buf).rotate(angle).jpeg({ quality: 90 }).toBuffer(); meta = await sharp(buf).metadata(); } else if (isBackSide) { buf = await sharp(buf).rotate(180).jpeg({ quality: 90 }).toBuffer(); meta = await sharp(buf).metadata(); } images.push({ page: i, buffer: buf, width: meta.width || Math.floor(viewport.width), height: meta.height || Math.floor(viewport.height), }); } await pdfDocument.destroy(); return images; } export async function imageToBase64(buffer: Buffer): Promise { const processed = await sharp(buffer) .resize(1200, undefined, { withoutEnlargement: true }) .jpeg({ quality: 85 }) .toBuffer(); return processed.toString("base64"); } export async function processUploadedImage(buffer: Buffer): Promise { return sharp(buffer) .resize(1600, undefined, { withoutEnlargement: true }) .normalize() .sharpen() .jpeg({ quality: 90 }) .toBuffer(); }