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.

This commit is contained in:
2026-07-13 15:12:54 -04:00
parent e2d8dee1a6
commit a929a8d859
5 changed files with 234 additions and 7 deletions

View File

@ -122,6 +122,32 @@ switch ($strAction) {
$tabRetour = array('state' => 'ok', 'html' => $strForm); $tabRetour = array('state' => 'ok', 'html' => $strForm);
break; 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': case 'bib_duplicate_check':
// Controle "dossard en double" dans l'evenement (avant enregistrement legacy). // Controle "dossard en double" dans l'evenement (avant enregistrement legacy).
// Ne modifie rien : renvoie qui porte deja ce dossard + la politique a appliquer. // Ne modifie rien : renvoie qui porte deja ce dossard + la politique a appliquer.

View File

@ -755,7 +755,11 @@
if (self.ocrBusy || activeScan !== self) { if (self.ocrBusy || activeScan !== self) {
return; 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); self.setStatus(scanMsg(self.panel, 'data-msg-loading', 'Initialisation (1re fois)...', 'Initializing (first time)...'), true);
return; return;
} }
@ -848,7 +852,10 @@
BibScanSession.prototype.runOcr = function () { BibScanSession.prototype.runOcr = function () {
var self = this; 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; return;
} }
self.ocrBusy = true; self.ocrBusy = true;
@ -858,6 +865,62 @@
} }
self.setStatus(scanMsg(self.panel, 'data-msg-scanning', 'Lecture en cours...', 'Reading...'), true); 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) { grabOcrCanvas(self).then(function (canvas) {
if (!canvas) { if (!canvas) {
throw new Error('capture'); throw new Error('capture');
@ -939,14 +1002,16 @@
}, },
audio: false 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) { if (activeScan !== self) {
results[0].getTracks().forEach(function (track) { track.stop(); }); results[0].getTracks().forEach(function (track) { track.stop(); });
return; return;
} }
self.workerReady = true; self.workerReady = cfg.bibOcrCloud ? true : !!results[1];
self.stream = results[0]; self.stream = results[0];
self.video = document.createElement('video'); self.video = document.createElement('video');
self.video.setAttribute('playsinline', 'true'); 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(); preloadOcrWorker();
} }

View File

@ -1467,6 +1467,131 @@ function fxInscrGestionRenderQrPanel($intEveId, $strLangue) {
echo '</div></div></details>'; echo '</div></div></details>';
} }
/**
* 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 16 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) { function fxInscrGestionRenderBibScanBlock($strBibInputId, $strLangue) {
echo '<div class="inscr-gestion-bib-scan-panel" hidden'; echo '<div class="inscr-gestion-bib-scan-panel" hidden';
echo ' data-bib-input-id="' . fxInscrGestionEsc($strBibInputId) . '"'; echo ' data-bib-input-id="' . fxInscrGestionEsc($strBibInputId) . '"';

View File

@ -7,10 +7,17 @@
* Constantes * * Constantes *
* *
**************/ **************/
define('_VERSION_CODE', '4.72.797'); define('_VERSION_CODE', '4.72.798');
define('_DATE_CODE', '2026-07-13'); define('_DATE_CODE', '2026-07-13');
//MSIN-4290 //MSIN-4290
define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');
// MSIN-4401 — OCR dossard : Google Cloud Vision (TEXT_DETECTION).
// Sans cette cle, le scan cloud est desactive (pas de Tesseract hasardeux sur mobile).
// Creer une cle API Vision (restreindre par IP / referer) et coller ici.
if (!defined('MSIN_BIB_OCR_VISION_API_KEY')) {
define('MSIN_BIB_OCR_VISION_API_KEY', '');
}
// Outil temporaire ms1_kc_set.php — modifier key_chain_http (lien /kc/{pk}/) // Outil temporaire ms1_kc_set.php — modifier key_chain_http (lien /kc/{pk}/)
// KEYCHAIN_HTTP_EDIT_SECRET : paramètre ?k= dans l'URL (mot de passe d'accès à la page) // KEYCHAIN_HTTP_EDIT_SECRET : paramètre ?k= dans l'URL (mot de passe d'accès à la page)
// KEYCHAIN_HTTP_EDIT_SHIFT : ajouté au numéro saisi (ex. 8 → pk 108 en base) // KEYCHAIN_HTTP_EDIT_SHIFT : ajouté au numéro saisi (ex. 8 → pk 108 en base)

View File

@ -42,6 +42,10 @@ if (!function_exists('fxV2RegisterScript')) {
return [ return [
'domain' => $vDomaine, 'domain' => $vDomaine,
'lang' => $strLangue, '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), 'preloaderWait' => afficheTexte('preloader_wait', 0, 0, 1),
'refundCancelHint' => ($strLangue === 'en') 'refundCancelHint' => ($strLangue === 'en')
? 'Your transaction has been refunded. If you also want to cancel it in the registration system:' ? 'Your transaction has been refunded. If you also want to cancel it in the registration system:'