From a929a8d859172aabff05b5b2672290f72795e47f Mon Sep 17 00:00:00 2001 From: stephan Date: Mon, 13 Jul 2026 15:12:54 -0400 Subject: [PATCH] Implement OCR functionality for bib numbers using Google Cloud Vision. Introduce new methods for enabling OCR, processing images, and extracting numbers from OCR text. Update JavaScript to handle cloud-based OCR processing and improve error handling. Increment version code to 4.72.798 to reflect these changes. --- ajax_inscr_gestion.php | 26 ++++++ js/v2/inscr-gestion.js | 77 +++++++++++++++-- php/inc_fx_inscriptions_gestion.php | 125 ++++++++++++++++++++++++++++ php/inc_settings.php | 9 +- php/inc_v2_assets.php | 4 + 5 files changed, 234 insertions(+), 7 deletions(-) diff --git a/ajax_inscr_gestion.php b/ajax_inscr_gestion.php index e0f41ba..d1b4a32 100644 --- a/ajax_inscr_gestion.php +++ b/ajax_inscr_gestion.php @@ -122,6 +122,32 @@ switch ($strAction) { $tabRetour = array('state' => 'ok', 'html' => $strForm); break; + case 'bib_ocr': + // MSIN-4401 — Lecture numero dossard via Google Cloud Vision (image JPEG base64). + if (!fxInscrGestionBibOcrCloudEnabled()) { + $tabRetour = array('state' => 'error', 'message' => 'ocr_not_configured'); + break; + } + $strImage = (string)($_POST['image'] ?? ''); + $arrOcr = fxInscrGestionVisionReadBibNumber($strImage); + if (!empty($arrOcr['ok']) && !empty($arrOcr['number'])) { + $tabRetour = array( + 'state' => 'ok', + 'number' => (string)$arrOcr['number'], + ); + break; + } + $strSeen = ''; + if (!empty($arrOcr['text'])) { + $strSeen = preg_replace('/\D+/', '', (string)$arrOcr['text']); + } + $tabRetour = array( + 'state' => 'error', + 'message' => (string)($arrOcr['message'] ?? 'no_number'), + 'seen' => $strSeen, + ); + break; + case 'bib_duplicate_check': // Controle "dossard en double" dans l'evenement (avant enregistrement legacy). // Ne modifie rien : renvoie qui porte deja ce dossard + la politique a appliquer. diff --git a/js/v2/inscr-gestion.js b/js/v2/inscr-gestion.js index 7889aff..f89e34f 100644 --- a/js/v2/inscr-gestion.js +++ b/js/v2/inscr-gestion.js @@ -755,7 +755,11 @@ if (self.ocrBusy || activeScan !== self) { return; } - if (!self.workerReady || !self.video) { + if (!self.video) { + self.setStatus(scanMsg(self.panel, 'data-msg-loading', 'Initialisation (1re fois)...', 'Initializing (first time)...'), true); + return; + } + if (!cfg.bibOcrCloud && !self.workerReady) { self.setStatus(scanMsg(self.panel, 'data-msg-loading', 'Initialisation (1re fois)...', 'Initializing (first time)...'), true); return; } @@ -848,7 +852,10 @@ BibScanSession.prototype.runOcr = function () { var self = this; - if (self.ocrBusy || !self.video || activeScan !== self || !self.workerReady) { + if (self.ocrBusy || !self.video || activeScan !== self) { + return; + } + if (!cfg.bibOcrCloud && !self.workerReady) { return; } self.ocrBusy = true; @@ -858,6 +865,62 @@ } self.setStatus(scanMsg(self.panel, 'data-msg-scanning', 'Lecture en cours...', 'Reading...'), true); + // MSIN-4401 — Chemin produit : photo → Google Vision (serveur). Plus fiable que Tesseract mobile. + if (cfg.bibOcrCloud) { + grabOcrCanvas(self).then(function (canvas) { + if (!canvas) { + throw new Error('capture'); + } + var dataUrl = canvas.toDataURL('image/jpeg', 0.92); + return fetch((cfg.domain || '') + '/ajax_inscr_gestion.php', { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, + body: 'a=bib_ocr&lang=' + encodeURIComponent(cfg.lang || 'fr') + + '&image=' + encodeURIComponent(dataUrl) + }).then(function (res) { + return res.json(); + }); + }).then(function (result) { + if (fxInscrRedirectIfSessionExpired(result)) { + return; + } + if (result && result.state === 'ok' && result.number) { + self.applyNumber(String(result.number)); + return; + } + if (result && result.message === 'ocr_not_configured') { + self.setStatus( + (document.documentElement.lang === 'en') + ? 'OCR not configured (Vision API key missing).' + : 'OCR non configuré (clé Vision manquante).', + false + ); + } else { + var base = scanMsg(self.panel, 'data-msg-invalid', 'Numéro illisible. Cadrez un seul numéro.', 'Unreadable number. Frame a single number.'); + if (result && result.seen) { + base += ' (' + result.seen + ')'; + } + self.setStatus(base, false); + } + window.setTimeout(function () { + if (activeScan === self) { + self.showReady(); + } + }, 2200); + }).catch(function () { + self.setStatus(scanMsg(self.panel, 'data-msg-invalid', 'Numéro illisible. Cadrez un seul numéro.', 'Unreadable number. Frame a single number.'), false); + window.setTimeout(function () { + if (activeScan === self) { + self.showReady(); + } + }, 2200); + }).then(function () { + self.ocrBusy = false; + }); + return; + } + grabOcrCanvas(self).then(function (canvas) { if (!canvas) { throw new Error('capture'); @@ -939,14 +1002,16 @@ }, audio: false }); - var workerPromise = getGlobalOcrWorker(); + var readyPromise = cfg.bibOcrCloud + ? cameraPromise.then(function (stream) { return [stream, null]; }) + : Promise.all([cameraPromise, getGlobalOcrWorker()]); - Promise.all([cameraPromise, workerPromise]).then(function (results) { + readyPromise.then(function (results) { if (activeScan !== self) { results[0].getTracks().forEach(function (track) { track.stop(); }); return; } - self.workerReady = true; + self.workerReady = cfg.bibOcrCloud ? true : !!results[1]; self.stream = results[0]; self.video = document.createElement('video'); self.video.setAttribute('playsinline', 'true'); @@ -970,7 +1035,7 @@ }); }; - if (document.querySelector('.inscr-gestion-bib-scan-toggle')) { + if (document.querySelector('.inscr-gestion-bib-scan-toggle') && !cfg.bibOcrCloud) { preloadOcrWorker(); } diff --git a/php/inc_fx_inscriptions_gestion.php b/php/inc_fx_inscriptions_gestion.php index 3e4e81e..d3a45ad 100644 --- a/php/inc_fx_inscriptions_gestion.php +++ b/php/inc_fx_inscriptions_gestion.php @@ -1467,6 +1467,131 @@ function fxInscrGestionRenderQrPanel($intEveId, $strLangue) { echo ''; } +/** + * MSIN-4401 — OCR dossard via Google Cloud Vision (serveur). + * Solution produit : ~95 % sur photos reelles vs ~50 % Tesseract.js mobile. + */ +function fxInscrGestionBibOcrCloudEnabled() { + return defined('MSIN_BIB_OCR_VISION_API_KEY') + && trim((string)MSIN_BIB_OCR_VISION_API_KEY) !== ''; +} + +/** Extraire un numero de dossard depuis du texte OCR (plus long groupe 1–6 chiffres). */ +function fxInscrGestionExtractBibNumberFromOcr($strRaw) { + if (!preg_match_all('/\d+/', (string)$strRaw, $arrM) || empty($arrM[0])) { + return ''; + } + $arrMatches = $arrM[0]; + $blnAllSingles = true; + foreach ($arrMatches as $strM) { + if (strlen($strM) !== 1) { + $blnAllSingles = false; + break; + } + } + if ($blnAllSingles) { + $strJoined = implode('', $arrMatches); + return (strlen($strJoined) >= 1 && strlen($strJoined) <= 6) ? $strJoined : ''; + } + $strBest = ''; + foreach ($arrMatches as $strM) { + $intLen = strlen($strM); + if ($intLen <= 6 && $intLen > strlen($strBest)) { + $strBest = $strM; + } + } + return $strBest; +} + +/** + * Appel REST Vision TEXT_DETECTION (sans SDK Composer). + * $strImageBase64 = contenu image (jpeg/png), sans prefixe data:. + * @return array{ok:bool, text?:string, number?:string, message?:string} + */ +function fxInscrGestionVisionReadBibNumber($strImageBase64) { + $strKey = defined('MSIN_BIB_OCR_VISION_API_KEY') + ? trim((string)MSIN_BIB_OCR_VISION_API_KEY) + : ''; + if ($strKey === '') { + return array('ok' => false, 'message' => 'ocr_not_configured'); + } + + $strImageBase64 = preg_replace('#^data:image/[^;]+;base64,#', '', (string)$strImageBase64); + $strImageBase64 = preg_replace('/\s+/', '', $strImageBase64); + if ($strImageBase64 === '' || strlen($strImageBase64) > 5 * 1024 * 1024) { + return array('ok' => false, 'message' => 'image_invalid'); + } + + $strUrl = 'https://vision.googleapis.com/v1/images:annotate?key=' . rawurlencode($strKey); + $arrPayload = array( + 'requests' => array( + array( + 'image' => array('content' => $strImageBase64), + 'features' => array( + array('type' => 'TEXT_DETECTION', 'maxResults' => 10), + ), + ), + ), + ); + $strBody = json_encode($arrPayload); + + $strResponse = ''; + $intHttp = 0; + if (function_exists('curl_init')) { + $ch = curl_init($strUrl); + curl_setopt_array($ch, array( + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => array('Content-Type: application/json'), + CURLOPT_POSTFIELDS => $strBody, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 20, + )); + $strResponse = (string)curl_exec($ch); + $intHttp = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + } else { + $ctx = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => $strBody, + 'timeout' => 20, + ), + )); + $strResponse = (string)@file_get_contents($strUrl, false, $ctx); + if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) { + $intHttp = (int)$m[1]; + } + } + + if ($strResponse === '' || ($intHttp > 0 && $intHttp >= 400)) { + error_log('[bib_ocr] Vision HTTP ' . $intHttp . ' body=' . substr($strResponse, 0, 300)); + return array('ok' => false, 'message' => 'vision_http'); + } + + $arrJson = json_decode($strResponse, true); + if (!is_array($arrJson)) { + return array('ok' => false, 'message' => 'vision_parse'); + } + if (!empty($arrJson['error']['message'])) { + error_log('[bib_ocr] Vision error: ' . $arrJson['error']['message']); + return array('ok' => false, 'message' => 'vision_api'); + } + + $strText = ''; + if (!empty($arrJson['responses'][0]['textAnnotations'][0]['description'])) { + $strText = (string)$arrJson['responses'][0]['textAnnotations'][0]['description']; + } elseif (!empty($arrJson['responses'][0]['fullTextAnnotation']['text'])) { + $strText = (string)$arrJson['responses'][0]['fullTextAnnotation']['text']; + } + + $strNum = fxInscrGestionExtractBibNumberFromOcr($strText); + if ($strNum === '') { + return array('ok' => false, 'message' => 'no_number', 'text' => $strText); + } + return array('ok' => true, 'number' => $strNum, 'text' => $strText); +} + function fxInscrGestionRenderBibScanBlock($strBibInputId, $strLangue) { echo '
$vDomaine, 'lang' => $strLangue, + // MSIN-4401 — true = OCR dossard via serveur (Vision), pas Tesseract local. + 'bibOcrCloud' => function_exists('fxInscrGestionBibOcrCloudEnabled') + ? (bool)fxInscrGestionBibOcrCloudEnabled() + : false, 'preloaderWait' => afficheTexte('preloader_wait', 0, 0, 1), 'refundCancelHint' => ($strLangue === 'en') ? 'Your transaction has been refunded. If you also want to cancel it in the registration system:'