551 lines
23 KiB
HTML
551 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Vérification locale de carte d'identité (OCR + MRZ)</title>
|
||
<style>
|
||
body {
|
||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Helvetica, Arial, sans-serif;
|
||
line-height: 1.45;
|
||
margin: 24px
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.35rem;
|
||
margin: 0 0 8px
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1.05rem;
|
||
margin: 16px 0 8px
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap
|
||
}
|
||
|
||
.col {
|
||
flex: 1 1 360px
|
||
}
|
||
|
||
.box {
|
||
border: 1px solid #ccc;
|
||
border-radius: 8px;
|
||
padding: 12px
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin: 8px 0 4px
|
||
}
|
||
|
||
img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px
|
||
}
|
||
|
||
pre {
|
||
white-space: pre-wrap;
|
||
background: #fafafa;
|
||
border: 1px solid #eee;
|
||
padding: 8px;
|
||
border-radius: 6px;
|
||
max-height: 260px;
|
||
overflow: auto
|
||
}
|
||
|
||
table {
|
||
border-collapse: collapse;
|
||
width: 100%
|
||
}
|
||
|
||
th,
|
||
td {
|
||
border: 1px solid #ddd;
|
||
padding: 6px 8px;
|
||
text-align: left
|
||
}
|
||
|
||
.ok {
|
||
color: #156f00;
|
||
font-weight: 600
|
||
}
|
||
|
||
.ko {
|
||
color: #9c1515;
|
||
font-weight: 600
|
||
}
|
||
|
||
.warn {
|
||
color: #7a5d00;
|
||
font-weight: 600
|
||
}
|
||
|
||
.kv {
|
||
font-family: ui-monospace, Consolas, Monaco, monospace
|
||
}
|
||
|
||
.muted {
|
||
color: #555
|
||
}
|
||
|
||
.progress {
|
||
height: 10px;
|
||
background: #eee;
|
||
border-radius: 999px;
|
||
overflow: hidden
|
||
}
|
||
|
||
.bar {
|
||
height: 100%;
|
||
width: 0%;
|
||
background: #999;
|
||
transition: width .2s
|
||
}
|
||
|
||
.mono {
|
||
font-family: ui-monospace, Consolas, Monaco, monospace
|
||
}
|
||
|
||
code {
|
||
font-family: ui-monospace, Consolas, Monaco, monospace
|
||
}
|
||
|
||
.hint {
|
||
font-size: .92rem;
|
||
margin-top: 8px
|
||
}
|
||
</style>
|
||
|
||
<!-- Tesseract.js auto-hébergé pour éviter les erreurs MIME depuis certains CDN -->
|
||
<script src="/vendor/tesseract/tesseract.min.js"></script>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>Vérification locale de carte d'identité (OCR + MRZ)</h1>
|
||
|
||
<div class="row">
|
||
<div class="col box">
|
||
<label for="file">Image de la pièce (photo/scan, MRZ nette)</label>
|
||
<input id="file" type="file" accept="image/*">
|
||
<div class="progress" style="margin:12px 0">
|
||
<div id="bar" class="bar"></div>
|
||
</div>
|
||
<button id="run">Analyser l'image</button>
|
||
<div class="hint muted">
|
||
Conseils : recadrer pour que la MRZ (2×44 ou 3×30 caractères) soit horizontale et contrastée. Éviter les reflets.
|
||
</div>
|
||
<h2>Aperçu</h2>
|
||
<img id="preview" alt="">
|
||
<h2>Texte OCR (brut)</h2>
|
||
<pre id="raw"></pre>
|
||
</div>
|
||
|
||
<div class="col box">
|
||
<h2>Résultats</h2>
|
||
<table id="results">
|
||
<tbody></tbody>
|
||
</table>
|
||
<h2>Score de vraisemblance</h2>
|
||
<div id="scoreBlock">
|
||
<div class="kv">Score total : <span id="score">–</span> / 100</div>
|
||
<pre id="scoreDetails"></pre>
|
||
</div>
|
||
<h2>Données structurées</h2>
|
||
<pre id="json"></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ---------- paramètres Tesseract.js épinglés pour usage en HTML statique ----------
|
||
// Chemins locaux pour worker et core
|
||
const WORKER_PATH = '/vendor/tesseract/worker.min.js';
|
||
const CORE_BASE = '/vendor/tesseract-core/'; // fichiers à la racine du module
|
||
const LANG_PATH = '/vendor/tessdata';
|
||
|
||
// ---------- éléments UI ----------
|
||
const fileInput = document.getElementById( 'file' );
|
||
const preview = document.getElementById( 'preview' );
|
||
const bar = document.getElementById( 'bar' );
|
||
const raw = document.getElementById( 'raw' );
|
||
const resultsBody = document.querySelector( '#results tbody' );
|
||
const scoreEl = document.getElementById( 'score' );
|
||
const scoreDetails = document.getElementById( 'scoreDetails' );
|
||
const jsonOut = document.getElementById( 'json' );
|
||
|
||
fileInput.addEventListener( 'change', e => {
|
||
const f = e.target.files[ 0 ];
|
||
if ( !f ) return;
|
||
const url = URL.createObjectURL( f );
|
||
preview.src = url;
|
||
raw.textContent = '';
|
||
} );
|
||
|
||
// ---------- OCR correctement initialisé ----------
|
||
async function recognizeImage ( file ) {
|
||
// création du worker avec chemins explicites pour fonctionner en file://
|
||
|
||
const worker = await Tesseract.createWorker( {
|
||
logger: m => {
|
||
if ( m.status === 'recognizing text' && m.progress != null ) {
|
||
bar.style.width = Math.round( m.progress * 100 ) + '%';
|
||
}
|
||
},
|
||
workerPath: WORKER_PATH,
|
||
corePath: CORE_BASE,
|
||
langPath: LANG_PATH,
|
||
} );
|
||
|
||
try {
|
||
// langues
|
||
await worker.loadLanguage( 'fra+eng' );
|
||
await worker.initialize( 'fra+eng' );
|
||
// reconnaissance
|
||
return await worker.recognize( file );
|
||
} finally {
|
||
await worker.terminate();
|
||
}
|
||
}
|
||
|
||
document.getElementById( 'run' ).addEventListener( 'click', async () => {
|
||
const f = fileInput.files[ 0 ];
|
||
if ( !f ) { alert( 'Choisir une image.' ); return; }
|
||
if ( !f.type.startsWith( 'image/' ) ) { alert( 'Fichier non image.' ); return; }
|
||
if ( f.size < 1024 ) { alert( 'Fichier trop petit ou vide.' ); return; }
|
||
|
||
clearOutputs();
|
||
|
||
try {
|
||
const res = await recognizeImage( f );
|
||
const text = res?.data?.text || '';
|
||
raw.textContent = text;
|
||
const analysis = analyzeText( text );
|
||
renderAnalysis( analysis );
|
||
} catch ( err ) {
|
||
console.error( err );
|
||
alert( 'Échec OCR. Vérifier l’accès réseau au CDN pour charger le modèle, ou tester sous serveur local.' );
|
||
}
|
||
} );
|
||
|
||
// ---------- analyse MRZ (ICAO Doc 9303 TD1/TD3) ----------
|
||
const ISO3 = {
|
||
FRA: "France", DZA: "Algérie", MAR: "Maroc", TUN: "Tunisie", DEU: "Allemagne", ESP: "Espagne", ITA: "Italie",
|
||
PRT: "Portugal", BEL: "Belgique", NLD: "Pays-Bas", LUX: "Luxembourg", CHE: "Suisse", GBR: "Royaume-Uni",
|
||
IRL: "Irlande", USA: "États-Unis", CAN: "Canada", MEX: "Mexique", BRA: "Brésil", ARG: "Argentine",
|
||
CHN: "Chine", JPN: "Japon", KOR: "Corée du Sud", IND: "Inde", RUS: "Russie", UKR: "Ukraine",
|
||
POL: "Pologne", CZE: "Tchéquie", SVK: "Slovaquie", HUN: "Hongrie", AUT: "Autriche", SWE: "Suède",
|
||
NOR: "Norvège", DNK: "Danemark", FIN: "Finlande", ISL: "Islande", GRC: "Grèce", ROU: "Roumanie",
|
||
BGR: "Bulgarie", HRV: "Croatie", SVN: "Slovénie", EST: "Estonie", LVA: "Lettonie", LTU: "Lituanie",
|
||
CYP: "Chypre", MLT: "Malte"
|
||
};
|
||
function isoName ( code ) { return ISO3[ code ]?.toString() || null; }
|
||
|
||
function analyzeText ( text ) {
|
||
const norm = text
|
||
.toUpperCase()
|
||
.replace( /[^\x20-\x7E]/g, '' )
|
||
.replace( /[ ]+/g, ' ' )
|
||
.replace( /\u00AB|\u00BB/g, '<' )
|
||
.replace( /[|]/g, '<' );
|
||
|
||
const lines = norm.split( /\r?\n/ ).map( l => l.trim() ).filter( Boolean );
|
||
const joined = lines.join( '\n' );
|
||
|
||
const td1 = findTD1( lines );
|
||
const td3 = findTD3( lines );
|
||
|
||
let mrz = null;
|
||
let format = null;
|
||
if ( td1 ) { mrz = td1; format = 'TD1'; }
|
||
else if ( td3 ) { mrz = td3; format = 'TD3'; }
|
||
|
||
let parsed = null, checks = null;
|
||
if ( mrz ) {
|
||
if ( format === 'TD1' ) { parsed = parseTD1( mrz ); checks = checkTD1( parsed ); }
|
||
else { parsed = parseTD3( mrz ); checks = checkTD3( parsed ); }
|
||
}
|
||
|
||
const allowed = /[A-Z0-9<]/;
|
||
const mrzStr = mrz ? mrz.join( '\n' ) : joined;
|
||
const mrzChars = mrzStr.replace( /\s+/g, '' ).split( '' );
|
||
const allowedCount = mrzChars.filter( ch => allowed.test( ch ) ).length;
|
||
const allowedRatio = mrzChars.length ? allowedCount / mrzChars.length : 0;
|
||
|
||
const plausibility = scorePlausibility( {
|
||
hasMRZ: !!mrz, format, checks, parsed, allowedRatio
|
||
} );
|
||
|
||
return { norm, lines, mrz, format, parsed, checks, allowedRatio, plausibility };
|
||
}
|
||
|
||
function findTD1 ( lines ) {
|
||
for ( let i = 0; i < lines.length - 2; i++ ) {
|
||
const a = lines[ i ], b = lines[ i + 1 ], c = lines[ i + 2 ];
|
||
if ( isMRZLen( a, 30 ) && isMRZLen( b, 30 ) && isMRZLen( c, 30 ) ) return [ a.slice( 0, 30 ), b.slice( 0, 30 ), c.slice( 0, 30 ) ];
|
||
}
|
||
return null;
|
||
}
|
||
function findTD3 ( lines ) {
|
||
for ( let i = 0; i < lines.length - 1; i++ ) {
|
||
const a = lines[ i ], b = lines[ i + 1 ];
|
||
if ( isMRZLen( a, 44 ) && isMRZLen( b, 44 ) ) return [ a.slice( 0, 44 ), b.slice( 0, 44 ) ];
|
||
}
|
||
return null;
|
||
}
|
||
function isMRZLen ( line, len ) {
|
||
return line && line.length >= len && /^[A-Z0-9<]+$/.test( line.slice( 0, len ) );
|
||
}
|
||
|
||
function parseTD1 ( [ L1, L2, L3 ] ) {
|
||
L1 = L1.padEnd( 30, '<' ); L2 = L2.padEnd( 30, '<' ); L3 = L3.padEnd( 30, '<' );
|
||
const docCode = L1.slice( 0, 2 );
|
||
const issuing = L1.slice( 2, 5 );
|
||
const namesRaw = L1.slice( 5, 30 ).replace( /<+/g, ' ' ).trim();
|
||
const [ surname, given ] = splitNames( namesRaw );
|
||
|
||
const docNumber = L2.slice( 0, 9 ).replace( /</g, '' );
|
||
const docNumberChk = L2[ 9 ];
|
||
const nationality = L2.slice( 10, 13 );
|
||
const birth = L2.slice( 13, 19 );
|
||
const birthChk = L2[ 19 ];
|
||
const sex = L2[ 20 ];
|
||
const expiry = L2.slice( 21, 27 );
|
||
const expiryChk = L2[ 27 ];
|
||
const optA = L2.slice( 28, 30 );
|
||
const optB = L3.slice( 0, 29 );
|
||
const compositeChk = L3[ 29 ];
|
||
|
||
const optional = ( optA + optB ).replace( /<+/g, '' ).trim() || null;
|
||
|
||
return {
|
||
format: 'TD1', docCode, issuing, surname, given,
|
||
docNumber, docNumberChk, nationality, birth, birthChk,
|
||
sex, expiry, expiryChk, optional, compositeChk,
|
||
raw: [ L1, L2, L3 ]
|
||
};
|
||
}
|
||
|
||
function parseTD3 ( [ L1, L2 ] ) {
|
||
L1 = L1.padEnd( 44, '<' ); L2 = L2.padEnd( 44, '<' );
|
||
const docCode = L1.slice( 0, 2 );
|
||
const issuing = L1.slice( 2, 5 );
|
||
const namesRaw = L1.slice( 5, 44 ).replace( /<+/g, ' ' ).trim();
|
||
const [ surname, given ] = splitNames( namesRaw );
|
||
|
||
const docNumber = L2.slice( 0, 9 ).replace( /</g, '' );
|
||
const docNumberChk = L2[ 9 ];
|
||
const nationality = L2.slice( 10, 13 );
|
||
const birth = L2.slice( 13, 19 );
|
||
const birthChk = L2[ 19 ];
|
||
const sex = L2[ 20 ];
|
||
const expiry = L2.slice( 21, 27 );
|
||
const expiryChk = L2[ 27 ];
|
||
const personal = L2.slice( 28, 42 );
|
||
const compositeChk = L2[ 43 ];
|
||
|
||
return {
|
||
format: 'TD3', docCode, issuing, surname, given,
|
||
docNumber, docNumberChk, nationality, birth, birthChk,
|
||
sex, expiry, expiryChk, optional: personal.replace( /<+/g, '' ).trim() || null,
|
||
compositeChk, raw: [ L1, L2 ]
|
||
};
|
||
}
|
||
|
||
function splitNames ( full ) {
|
||
if ( full.includes( '<<' ) ) {
|
||
const [ sn, gn ] = full.split( '<<' );
|
||
return [ sn.replace( /<+/g, ' ' ).trim(), gn?.replace( /<+/g, ' ' ).trim() || null ];
|
||
}
|
||
const words = full.split( ' ' ).filter( Boolean );
|
||
if ( words.length > 1 ) return [ words[ 0 ], words.slice( 1 ).join( ' ' ) ];
|
||
return [ full, null ];
|
||
}
|
||
|
||
// chiffres de contrôle (pondérations 7-3-1)
|
||
const weights = [ 7, 3, 1 ];
|
||
function charValue ( ch ) {
|
||
if ( ch >= '0' && ch <= '9' ) return ch.charCodeAt( 0 ) - 48;
|
||
if ( ch >= 'A' && ch <= 'Z' ) return ch.charCodeAt( 0 ) - 55;
|
||
if ( ch === '<' ) return 0;
|
||
return 0;
|
||
}
|
||
function checkDigitFor ( str ) {
|
||
let sum = 0;
|
||
for ( let i = 0; i < str.length; i++ ) {
|
||
const v = charValue( str[ i ] );
|
||
const w = weights[ i % 3 ];
|
||
sum += v * w;
|
||
}
|
||
return String( sum % 10 );
|
||
}
|
||
|
||
function checkTD1 ( p ) {
|
||
if ( !p ) return null;
|
||
const L2 = p.raw[ 1 ].slice( 0, 30 );
|
||
const L3 = p.raw[ 2 ].slice( 0, 30 );
|
||
const cDoc = checkDigitFor( p.raw[ 1 ].slice( 0, 9 ) );
|
||
const cBirth = checkDigitFor( p.birth );
|
||
const cExp = checkDigitFor( p.expiry );
|
||
const compositeField = L2 + L3.slice( 0, 29 );
|
||
const cComp = checkDigitFor( compositeField );
|
||
return {
|
||
docNumber: { expected: cDoc, actual: p.docNumberChk, valid: cDoc === p.docNumberChk },
|
||
birth: { expected: cBirth, actual: p.birthChk, valid: cBirth === p.birthChk },
|
||
expiry: { expected: cExp, actual: p.expiryChk, valid: cExp === p.expiryChk },
|
||
composite: { expected: cComp, actual: p.compositeChk, valid: cComp === p.compositeChk }
|
||
};
|
||
}
|
||
|
||
function checkTD3 ( p ) {
|
||
if ( !p ) return null;
|
||
const cDoc = checkDigitFor( p.raw[ 1 ].slice( 0, 9 ) );
|
||
const cBirth = checkDigitFor( p.birth );
|
||
const cExp = checkDigitFor( p.expiry );
|
||
const compositeField = p.raw[ 1 ].slice( 0, 10 ) + p.raw[ 1 ].slice( 13, 20 ) + p.birthChk + p.raw[ 1 ].slice( 21, 28 ) + p.expiryChk + p.raw[ 1 ].slice( 28, 42 );
|
||
const cComp = checkDigitFor( compositeField );
|
||
return {
|
||
docNumber: { expected: cDoc, actual: p.docNumberChk, valid: cDoc === p.docNumberChk },
|
||
birth: { expected: cBirth, actual: p.birthChk, valid: cBirth === p.birthChk },
|
||
expiry: { expected: cExp, actual: p.expiryChk, valid: cExp === p.expiryChk },
|
||
composite: { expected: cComp, actual: p.compositeChk, valid: cComp === p.compositeChk }
|
||
};
|
||
}
|
||
|
||
function parseYYMMDD ( yyMMdd ) {
|
||
if ( !/^[0-9]{6}$/.test( yyMMdd ) ) return null;
|
||
const yy = parseInt( yyMMdd.slice( 0, 2 ), 10 );
|
||
const mm = parseInt( yyMMdd.slice( 2, 4 ), 10 );
|
||
const dd = parseInt( yyMMdd.slice( 4, 6 ), 10 );
|
||
if ( mm < 1 || mm > 12 || dd < 1 || dd > 31 ) return null;
|
||
const year = yy <= 29 ? 2000 + yy : 1900 + yy;
|
||
const date = new Date( Date.UTC( year, mm - 1, dd ) );
|
||
if ( date.getUTCMonth() !== mm - 1 || date.getUTCDate() !== dd ) return null;
|
||
return { year, month: mm, day: dd, iso: `${ year.toString().padStart( 4, '0' ) }-${ String( mm ).padStart( 2, '0' ) }-${ String( dd ).padStart( 2, '0' ) }` };
|
||
}
|
||
|
||
function ageFrom ( birthISO ) {
|
||
const d = new Date( birthISO ); const now = new Date();
|
||
let age = now.getUTCFullYear() - d.getUTCFullYear();
|
||
const m = now.getUTCMonth() - d.getUTCMonth();
|
||
if ( m < 0 || ( m === 0 && now.getUTCDate() < d.getUTCDate() ) ) age--;
|
||
return age;
|
||
}
|
||
|
||
function scorePlausibility ( { hasMRZ, format, checks, parsed, allowedRatio } ) {
|
||
let total = 0;
|
||
const lines = [];
|
||
const W = {
|
||
mrzPresent: 20, docNumber: 15, birth: 10, expiry: 10, composite: 15,
|
||
datesPlausible: 10, nationality: 5, issuing: 5, ocrClean: 10
|
||
};
|
||
|
||
if ( hasMRZ ) { total += W.mrzPresent; lines.push( `[+${ W.mrzPresent }] MRZ détectée (${ format }).` ); }
|
||
else { lines.push( `[+0] MRZ non détectée.` ); }
|
||
|
||
if ( checks ) {
|
||
if ( checks.docNumber.valid ) { total += W.docNumber; lines.push( `[+${ W.docNumber }] Numéro : chiffre de contrôle correct (${ checks.docNumber.actual }).` ); }
|
||
else { lines.push( `[+0] Numéro : attendu ${ checks.docNumber.expected }, obtenu ${ checks.docNumber.actual }.` ); }
|
||
|
||
if ( checks.birth.valid ) { total += W.birth; lines.push( `[+${ W.birth }] Date de naissance : chiffre de contrôle correct (${ checks.birth.actual }).` ); }
|
||
else { lines.push( `[+0] Naissance : attendu ${ checks.birth.expected }, obtenu ${ checks.birth.actual }.` ); }
|
||
|
||
if ( checks.expiry.valid ) { total += W.expiry; lines.push( `[+${ W.expiry }] Expiration : chiffre de contrôle correct (${ checks.expiry.actual }).` ); }
|
||
else { lines.push( `[+0] Expiration : attendu ${ checks.expiry.expected }, obtenu ${ checks.expiry.actual }.` ); }
|
||
|
||
if ( checks.composite.valid ) { total += W.composite; lines.push( `[+${ W.composite }] Contrôle composite correct.` ); }
|
||
else { lines.push( `[+0] Contrôle composite invalide.` ); }
|
||
} else {
|
||
lines.push( `[+0] Chiffres de contrôle non évalués.` );
|
||
}
|
||
|
||
let datesOK = false;
|
||
if ( parsed ) {
|
||
const b = parseYYMMDD( parsed.birth );
|
||
const e = parseYYMMDD( parsed.expiry );
|
||
if ( b && e ) {
|
||
const age = ageFrom( b.iso );
|
||
const nowISO = new Date().toISOString().slice( 0, 10 );
|
||
const expPast = ( new Date( e.iso ) < new Date( nowISO ) );
|
||
const ageOK = age >= 0 && age <= 120;
|
||
datesOK = ageOK;
|
||
lines.push( `[info] Naissance : ${ b.iso } ⇒ âge ${ age } ans.` );
|
||
lines.push( `[info] Expiration : ${ e.iso } ⇒ ${ expPast ? 'document expiré' : 'valide à date' }.` );
|
||
}
|
||
}
|
||
if ( datesOK ) { total += W.datesPlausible; lines.push( `[+${ W.datesPlausible }] Dates plausibles (âge 0–120).` ); }
|
||
else { lines.push( `[+0] Dates non plausibles ou illisibles.` ); }
|
||
|
||
if ( parsed ) {
|
||
const nat = isoName( parsed.nationality );
|
||
const iss = isoName( parsed.issuing );
|
||
if ( nat ) { total += W.nationality; lines.push( `[+${ W.nationality }] Nationalité reconnue (${ parsed.nationality } = ${ nat }).` ); }
|
||
else { lines.push( `[+0] Code nationalité inconnu (${ parsed?.nationality || '–' }).` ); }
|
||
if ( iss ) { total += W.issuing; lines.push( `[+${ W.issuing }] Pays émetteur reconnu (${ parsed.issuing } = ${ iss }).` ); }
|
||
else { lines.push( `[+0] Code pays émetteur inconnu (${ parsed?.issuing || '–' }).` ); }
|
||
}
|
||
|
||
const ocrPts = Math.round( W.ocrClean * Math.min( 1, Math.max( 0, ( allowedRatio - 0.85 ) / 0.15 ) ) );
|
||
total += ocrPts;
|
||
lines.push( `[+${ ocrPts }] Propreté OCR (ratio caractères MRZ valides = ${ ( allowedRatio * 100 ).toFixed( 1 ) } %).` );
|
||
|
||
if ( total > 100 ) total = 100;
|
||
return { total, breakdown: lines.join( '\n' ) };
|
||
}
|
||
|
||
function renderAnalysis ( a ) {
|
||
resultsBody.innerHTML = '';
|
||
function row ( k, v, cls = '' ) { const tr = document.createElement( 'tr' ); tr.innerHTML = `<th>${ k }</th><td class="kv ${ cls }">${ v ?? '–' }</td>`; resultsBody.appendChild( tr ); }
|
||
|
||
row( 'Format MRZ détecté', a.format || '—', a.mrz ? 'ok' : 'ko' );
|
||
if ( a.parsed ) {
|
||
const p = a.parsed, c = a.checks;
|
||
row( 'Code document', p.docCode );
|
||
row( 'Pays émetteur', `${ p.issuing } ${ isoName( p.issuing ) ? ' – ' + isoName( p.issuing ) : '' }` );
|
||
row( 'Nom', p.surname || '—' );
|
||
row( 'Prénoms', p.given || '—' );
|
||
row( 'Numéro document', p.docNumber || '—' );
|
||
if ( c ) row( 'Contrôle numéro', c.docNumber.valid ? 'valide' : `invalide (attendu ${ c.docNumber.expected }, obtenu ${ c.docNumber.actual })`, c.docNumber.valid ? 'ok' : 'ko' );
|
||
row( 'Nationalité', `${ p.nationality } ${ isoName( p.nationality ) ? ' – ' + isoName( p.nationality ) : '' }` );
|
||
const b = parseYYMMDD( p.birth );
|
||
if ( b ) { row( 'Naissance', `${ b.iso } (chiffre ${ a.checks?.birth?.actual || '–' })` ); row( 'Âge estimé', ageFrom( b.iso ) + ' ans' ); }
|
||
const e = parseYYMMDD( p.expiry );
|
||
if ( e ) { const expPast = new Date( e.iso ) < new Date( new Date().toISOString().slice( 0, 10 ) ); row( 'Expiration', `${ e.iso } (${ expPast ? 'expiré' : 'valide' })` ); }
|
||
row( 'Sexe (MRZ)', p.sex || '—' );
|
||
if ( a.checks ) {
|
||
row( 'Contrôle naissance', a.checks.birth.valid ? 'valide' : 'invalide', a.checks.birth.valid ? 'ok' : 'ko' );
|
||
row( 'Contrôle expiration', a.checks.expiry.valid ? 'valide' : 'invalide', a.checks.expiry.valid ? 'ok' : 'ko' );
|
||
row( 'Contrôle composite', a.checks.composite.valid ? 'valide' : 'invalide', a.checks.composite.valid ? 'ok' : 'ko' );
|
||
}
|
||
row( 'Données optionnelles', p.optional || '—' );
|
||
} else {
|
||
row( 'MRZ', 'non détectée', 'ko' );
|
||
}
|
||
scoreEl.textContent = a.plausibility.total.toString();
|
||
scoreDetails.textContent = a.plausibility.breakdown;
|
||
jsonOut.textContent = JSON.stringify( {
|
||
format: a.format || null,
|
||
parsed: a.parsed || null,
|
||
checks: a.checks || null,
|
||
allowedRatio: a.allowedRatio,
|
||
score: a.plausibility.total
|
||
}, null, 2 );
|
||
}
|
||
|
||
function clearOutputs () {
|
||
resultsBody.innerHTML = '';
|
||
raw.textContent = '';
|
||
scoreEl.textContent = '–';
|
||
scoreDetails.textContent = '';
|
||
jsonOut.textContent = '';
|
||
bar.style.width = '0%';
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |