import Tesseract from 'tesseract.js' import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs' import { join } from 'path' /** * Server-side OCR for mobile return page. * * POST /api/mobile/ocr-scan * Body: multipart form-data with 'image' file field (JPEG/PNG binary) * - Legacy fallback: JSON { image: "data:image/..." } (base64 data URL) * * Pipeline: * 1. Receive image (multipart preferred — avoids ~33% base64 bloat) * 2. Write to temp file in .tmp-ocr/ * 3. Run tesseract.js (deu+eng) with a hard timeout * 4. Return lines of recognized text * 5. Always clean up the temp file */ const OCR_TIMEOUT_MS = 60_000 const withTimeout = (promise: Promise, ms: number, label: string): Promise => { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms) promise.then( (v) => { clearTimeout(timer); resolve(v) }, (e) => { clearTimeout(timer); reject(e) } ) }) } export default defineEventHandler(async (event) => { const reqId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) console.log(`[OCR ${reqId}] Request received`) let imageBuffer: Buffer | null = null let filename = 'capture.jpg' const contentType = getRequestHeader(event, 'content-type') || '' try { if (contentType.includes('multipart/form-data')) { const formData = await readMultipartFormData(event) if (formData) { for (const part of formData) { if (part.name === 'image') { imageBuffer = part.data filename = part.filename || filename } } } console.log(`[OCR ${reqId}] Multipart parsed, size=${imageBuffer?.length ?? 0} bytes, filename=${filename}`) } else { // Legacy JSON base64 path const body = await readBody(event) const imageData: string | undefined = body?.image if (imageData && imageData.startsWith('data:image/')) { const base64 = imageData.split(',')[1] || '' imageBuffer = Buffer.from(base64, 'base64') const ext = imageData.slice(11, imageData.indexOf(';')) filename = `capture.${ext || 'jpg'}` console.log(`[OCR ${reqId}] JSON/base64 decoded, size=${imageBuffer.length} bytes`) } } } catch (err: any) { console.error(`[OCR ${reqId}] Failed to read request body:`, err?.message || err) throw createError({ statusCode: 400, statusMessage: 'Failed to read image from request' }) } if (!imageBuffer || imageBuffer.length === 0) { console.error(`[OCR ${reqId}] No image data in request`) throw createError({ statusCode: 400, statusMessage: 'Image data is required' }) } // Write to temp file so tesseract can read from disk (most reliable path) const tmpDir = join(process.cwd(), '.tmp-ocr') if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true }) const tmpPath = join(tmpDir, `${reqId}-${filename}`) try { writeFileSync(tmpPath, imageBuffer) console.log(`[OCR ${reqId}] Wrote temp file: ${tmpPath}`) } catch (err: any) { console.error(`[OCR ${reqId}] Failed to write temp file:`, err?.message || err) throw createError({ statusCode: 500, statusMessage: 'Failed to save image for processing' }) } try { console.log(`[OCR ${reqId}] Starting Tesseract.recognize (deu+eng)`) const started = Date.now() // Cache downloaded language data in .tmp-ocr/lang/ instead of project root const langCache = join(tmpDir, 'lang') if (!existsSync(langCache)) mkdirSync(langCache, { recursive: true }) const result = await withTimeout( Tesseract.recognize(tmpPath, 'deu+eng', { cachePath: langCache, langPath: 'https://tessdata.projectnaptha.com/4.0.0' } as any), OCR_TIMEOUT_MS, 'Tesseract.recognize' ) const text = result?.data?.text || '' const confidence = result?.data?.confidence || 0 console.log(`[OCR ${reqId}] Completed in ${Date.now() - started}ms, confidence=${confidence}, textLen=${text.length}`) const lines = text .split('\n') .map((line: string) => line.trim()) .filter((line: string) => line.length > 2) if (lines.length === 0) { return { success: false, message: 'No text detected in image', lines: [] } } return { success: true, lines, fullText: text, confidence } } catch (err: any) { console.error(`[OCR ${reqId}] Processing failed:`, err?.message || err, err?.stack) throw createError({ statusCode: 500, statusMessage: 'OCR processing failed: ' + (err?.message || 'Unknown error') }) } finally { try { if (existsSync(tmpPath)) unlinkSync(tmpPath) } catch {} } })