// Client-side crop thumbnails for the question bank. A bank question carries `bounds` (x,y,w,h) in the // 780px stacked-page canvas space and a `page` number; the source PDF renders at that same 780px width // (pdfLoader), so a thumbnail is just the page image cropped to the bounds. Renders each template's PDF // once (cached) and reuses it for all of that template's questions. No API/infra change. import { examRepository } from '../../services/exam/examRepository'; import { loadPdfPageImages, PdfPageImage } from './setup/pdfLoader'; const renderCache = new Map>(); const imageCache = new Map>(); function templateRender(templateId: string): Promise { let p = renderCache.get(templateId); if (!p) { p = examRepository.getTemplateSourcePdf(templateId).then((bytes) => loadPdfPageImages(bytes)); p.catch(() => renderCache.delete(templateId)); // let a failed render be retried renderCache.set(templateId, p); } return p; } function loadImage(src: string): Promise { let p = imageCache.get(src); if (!p) { p = new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('image decode failed')); img.src = src; }); imageCache.set(src, p); } return p; } export interface CropInput { templateId: string; page: number | null; bounds: Record | null; } // Returns a PNG data-URL cropped to the question, or null if it can't be produced (no bounds/page/PDF). export async function cropQuestion(input: CropInput, maxWidth = 320): Promise { const { templateId, page, bounds } = input; if (!bounds || !page) return null; const pages = await templateRender(templateId); const pageImg = pages.find((pg) => pg.pageNumber === page); if (!pageImg) return null; // stacked y-offset of this page (bounds.y is absolute across the vertically-stacked pages) let yTop = 0; for (const pg of pages) { if (pg.pageNumber === page) break; yTop += pg.height; } const sx = Math.max(0, Math.round(bounds.x ?? 0)); const sy = Math.max(0, Math.round((bounds.y ?? 0) - yTop)); const sw = Math.min(Math.round(bounds.w ?? pageImg.width), pageImg.width - sx); const sh = Math.min(Math.round(bounds.h ?? 80), pageImg.height - sy); if (sw <= 1 || sh <= 1) return null; const img = await loadImage(pageImg.src); const scale = Math.min(1, maxWidth / sw); const canvas = document.createElement('canvas'); canvas.width = Math.max(1, Math.round(sw * scale)); canvas.height = Math.max(1, Math.round(sh * scale)); const ctx = canvas.getContext('2d'); if (!ctx) return null; ctx.drawImage(img, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height); return canvas.toDataURL('image/png'); }