const sharp = require('sharp') const path = require('path') const fs = require('fs') const { execSync } = require('child_process') const { isCNIDocument, enhanceCNIPreprocessing, processCNIWithZones, decodeMRZ, } = require('./cniOcrEnhancer') /** * OCR amélioré avec support spécialisé pour les CNI * Combine Tesseract avec des techniques de preprocessing avancées */ // Fonction pour exécuter Tesseract avec stratégies multiples et choisir le meilleur async function runTesseractOCR(imageBuffer, options = {}) { const tempInput = path.join(__dirname, `temp_input_${Date.now()}.png`) const tempOutputBase = path.join(__dirname, `temp_output_${Date.now()}`) fs.writeFileSync(tempInput, imageBuffer) const strategies = [] const baseLang = options.language || 'fra' const basePsm = options.psm || '6' const baseOem = options.oem || '3' // Stratégies génériques strategies.push({ lang: baseLang, psm: basePsm, oem: baseOem }) strategies.push({ lang: baseLang, psm: '3', oem: baseOem }) strategies.push({ lang: baseLang, psm: '13', oem: baseOem }) // Si on cible MRZ/OCRB if ((options.language || '').includes('eng') || options.mrz) { // OCRB peut ne pas être installé; on tente eng+ocrb puis eng seul strategies.push({ lang: 'ocrb+eng', psm: '6', oem: baseOem }) strategies.push({ lang: 'ocrb+eng', psm: '8', oem: baseOem }) strategies.push({ lang: 'eng', psm: '6', oem: baseOem }) strategies.push({ lang: 'eng', psm: '8', oem: baseOem }) } let best = { text: '', score: -1, meta: null } for (let i = 0; i < strategies.length; i += 1) { const s = strategies[i] const tempOutput = `${tempOutputBase}_${i}` const cmd = `tesseract "${tempInput}" "${tempOutput}" -l ${s.lang} --psm ${s.psm} --oem ${s.oem}` try { execSync(cmd, { stdio: 'pipe' }) const t = fs.readFileSync(`${tempOutput}.txt`, 'utf8') const text = t.trim() // Heuristique de score: longueur utile et présence de caractères alphanumériques const alpha = (text.match(/[A-Za-z0-9]/g) || []).length const score = alpha + (text.includes('<<') ? 20 : 0) if (score > best.score) best = { text, score, meta: s } try { fs.unlinkSync(`${tempOutput}.txt`) } catch {} } catch (e) { // Essai suivant } } try { fs.unlinkSync(tempInput) } catch {} return { text: best.text, confidence: best.score > 0 ? 0.85 : 0.6, method: 'tesseract_multi', used: best.meta, } } // Fonction pour extraire le texte d'une image avec améliorations CNI async function extractTextFromImageEnhanced(inputPath) { try { console.log(`[ENHANCED_OCR] Début extraction: ${path.basename(inputPath)}`) // Vérifier si c'est une CNI const isCNI = await isCNIDocument(inputPath) console.log(`[ENHANCED_OCR] CNI détectée: ${isCNI}`) if (isCNI) { return await extractTextFromCNI(inputPath) } else { return await extractTextFromStandardDocument(inputPath) } } catch (error) { console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message) throw error } } // Fonction spécialisée pour l'extraction de texte des CNI async function extractTextFromCNI(inputPath) { try { console.log(`[CNI_OCR] Traitement CNI: ${path.basename(inputPath)}`) // Améliorer le preprocessing pour les CNI const enhancedImage = await enhanceCNIPreprocessing(inputPath) if (!enhancedImage) { throw new Error('Échec du preprocessing CNI') } // Traitement par zones const cniZones = await processCNIWithZones(inputPath) let combinedText = '' let mrzData = null // Extraire le texte de l'image améliorée const mainText = await runTesseractOCR(enhancedImage, { language: 'fra', psm: '6', // Mode uniforme de bloc de texte }) combinedText += mainText.text + '\n' // Si on a des zones, traiter chaque zone séparément if (cniZones && cniZones.zones) { for (const [zoneName, zoneImage] of Object.entries(cniZones.zones)) { try { const zoneText = await runTesseractOCR(zoneImage, { language: 'fra', psm: '8', // Mode mot unique }) combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n` console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`) } catch (zoneError) { console.warn(`[CNI_OCR] Erreur zone ${zoneName}:`, zoneError.message) } } } // Traiter la MRZ si disponible if (cniZones && cniZones.mrz) { try { const mrzText = await runTesseractOCR(cniZones.mrz, { language: 'ocrb+eng', psm: '6', mrz: true, }) combinedText += `[MRZ] ${mrzText.text}\n` // Décoder la MRZ mrzData = decodeMRZ(mrzText.text) if (mrzData) { combinedText += `[MRZ_DECODED] Nom: ${mrzData.surname}, Prénom: ${mrzData.givenNames}, Numéro: ${mrzData.documentNumber}\n` } } catch (mrzError) { console.warn(`[CNI_OCR] Erreur MRZ:`, mrzError.message) } } // Post-traitement du texte pour corriger les erreurs communes const processedText = postProcessCNIText(combinedText) console.log(`[CNI_OCR] Texte final: ${processedText.length} caractères`) return { text: processedText, confidence: 0.85, // Confiance élevée pour les CNI traitées method: 'cni_enhanced', mrzData: mrzData, zones: cniZones ? Object.keys(cniZones.zones || {}) : [], } } catch (error) { console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message) throw error } } // Fonction pour l'extraction de texte des documents standards async function extractTextFromStandardDocument(inputPath) { try { console.log(`[STANDARD_OCR] Traitement document standard: ${path.basename(inputPath)}`) // Preprocessing standard const image = sharp(inputPath) const metadata = await image.metadata() const processedImage = await image .resize({ width: Math.min(metadata.width * 2, 2000), height: Math.min(metadata.height * 2, 2000), fit: 'inside', withoutEnlargement: false, }) .grayscale() // Sharp attend des bornes entières 1..100 pour lower/upper (percentiles) // Ancien réglage (0.1/0.9) provoquait une erreur. On utilise 10/90. .normalize({ lower: 10, upper: 90 }) // Voir commentaire ci-dessus: clamp en 1..100 .normalize({ lower: 10, upper: 90 }) .sharpen() .png() .toBuffer() // OCR standard const result = await runTesseractOCR(processedImage, { language: 'fra', psm: '6', }) return { text: result.text, confidence: result.confidence, method: 'standard_enhanced', } } catch (error) { console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message) throw error } } // Fonction pour post-traiter le texte des CNI function postProcessCNIText(text) { try { let processedText = text // Corrections communes pour les CNI const corrections = [ // Corrections de caractères corrompus { from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' }, { from: /CARTE NATIONALE DIDENTITE/g, to: "CARTE NATIONALE D'IDENTITÉ" }, { from: /Ne :/g, to: 'N° :' }, { from: /Fe - 0/g, to: 'Féminin' }, { from: /Mele:/g, to: 'Mâle:' }, { from: /IDFRACANTUCCKKLLLLKLLLLLLLLLLLK/g, to: 'IDFRA' }, // Nettoyage des caractères parasites { from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g, to: ' ', }, // Normalisation des espaces { from: /\s+/g, to: ' ' }, { from: /^\s+|\s+$/g, to: '' }, ] // Appliquer les corrections for (const correction of corrections) { processedText = processedText.replace(correction.from, correction.to) } console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`) return processedText } catch (error) { console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message) return text } } module.exports = { extractTextFromImageEnhanced, extractTextFromCNI, extractTextFromStandardDocument, runTesseractOCR, postProcessCNIText, }