/** * Browser-side Tesseract OCR for Layer-1 card name strip recognition. * Uses tesseract.js workers (off main thread). Language data loads from CDN on first use. */ let workerPromise = null; async function getWorker() { if (!workerPromise) { workerPromise = (async () => { const { createWorker } = await import('tesseract.js'); const worker = await createWorker('eng', 1, { logger: () => {}, }); await worker.setParameters({ tessedit_pageseg_mode: '6', }); return worker; })(); } return workerPromise; } function cropNameStrip(imageDataUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const stripHeight = Math.max(1, Math.floor(img.height * 0.35)); const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = stripHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, img.width, stripHeight, 0, 0, img.width, stripHeight); resolve(canvas.toDataURL('image/jpeg', 0.92)); }; img.onerror = () => reject(new Error('Failed to load image for OCR')); img.src = imageDataUrl; }); } /** * OCR the top name strip of a card crop. * @param {string} imageDataUrl - full card crop data URL * @returns {Promise<{ text: string, confidence: number }>} */ export async function recognizeCardNameStrip(imageDataUrl) { if (typeof window === 'undefined') { return { text: '', confidence: 0 }; } try { const stripUrl = await cropNameStrip(imageDataUrl); const worker = await getWorker(); const { data } = await worker.recognize(stripUrl); return { text: (data.text || '').trim(), confidence: Math.round(data.confidence || 0), }; } catch (error) { console.warn('[ocr-worker] recognizeCardNameStrip failed:', error); return { text: '', confidence: 0 }; } } export async function terminateOcrWorker() { if (!workerPromise) return; try { const worker = await workerPromise; await worker.terminate(); } catch { // ignore teardown errors } workerPromise = null; }