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:
@ -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.
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 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) {
|
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) . '"';
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:'
|
||||||
|
|||||||
Reference in New Issue
Block a user