L'ISBN 9782846391009 n'était pas décodé par l'application, alors qu'il l'était par une application d'essai utilisant zbar, sur le même livre et le même appareil. ⚠️ Le raisonnement qui avait écarté cette piste était faux : CLAUDE.md concluait que « le décodeur n'y est pour rien » à partir de mesures de VITESSE (4-6 ms/frame). La vitesse ne dit rien du taux de réussite. Le décodage quitte donc le C# pour zbar compilé en WebAssembly, dont les assets viennent du paquet ZBar.Blazor. Ce que la bascule apporte, au-delà du code lu : - plus aucun pixel ne traverse le pont JS→C# (un byte[] par frame avant), seule la valeur décodée le fait ; - l'image ENTIÈRE est analysée, à 960 px, là où l'ancienne version recadrait sur la bande centrale à 640 px pour alléger ce transfert — c'était la seconde cause possible des codes non lus, elle disparaît ; - zbar.wasm est chargé À LA DEMANDE, à la première ouverture du scanner. ⚠️ Le composant ZBarCamera du paquet n'est PAS utilisé : il ouvre la caméra lui-même et avale les erreurs. On y perdrait les messages qui distinguent permission refusée, absence de caméra, caméra occupée et contexte non sécurisé, ainsi que facingMode environment (caméra arrière) et l'indication de résolution. Seuls zbar.js et zbar.wasm sont empruntés. Poids, comparaison de deux publish Release complets (somme brotli) : _framework 3 281 558 → 2 978 700 o (−296 Kio au démarrage) total 3 281 558 → 3 125 541 o (−152 Kio) L'écart vient de la traîne que ZXing imposait au trimmer : System.Text.RegularExpressions retombe de 98 374 à 7 137 o, et System.Runtime.Numerics disparaît. ⚠️ Coût assumé : six tests de décodage disparaissent avec IsbnScanner, le décodeur n'étant plus en C#. Le décodage a été vérifié dans le navigateur sur un EAN-13 rendu en canvas — 9782846391009 ressort bien en ZBAR_EAN13 — mais cela reste une vérification, pas un garde-fou. À confirmer avec le livre en main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
5.4 KiB
JavaScript
128 lines
5.4 KiB
JavaScript
// Rôle de ce fichier : ouvrir la caméra ET décoder les codes-barres avec zbar.
|
|
//
|
|
// ⚠️ Le décodage était en C# (ZXing.Net) jusqu'au 2026-08-19. Il en est sorti parce que
|
|
// ZXing.Net ne lisait pas des codes-barres que zbar lit, sur le même livre et le même
|
|
// appareil (voir CLAUDE.md). Conséquence heureuse : les pixels ne traversent plus le pont
|
|
// JS→C#, seule la valeur décodée le fait.
|
|
//
|
|
// On n'utilise PAS le composant ZBarCamera du paquet : il ouvre la caméra lui-même et avale
|
|
// les erreurs, alors que les messages ci-dessous sont ce qui permet à l'utilisateur de savoir
|
|
// ce qui lui manque. On n'emprunte au paquet que zbar.js et zbar.wasm.
|
|
|
|
let flux = null;
|
|
let video = null;
|
|
let canvas = null;
|
|
let ctx = null;
|
|
let scanner = null;
|
|
|
|
// Chargé à la première ouverture du scanner, pas au démarrage de l'application : zbar.wasm
|
|
// pèse ~139 Ko brotli, que personne n'a à télécharger pour consulter sa bibliothèque.
|
|
const CHEMIN_ZBAR = '_content/ZBar.Blazor/zbar.js';
|
|
|
|
let chargementZbar = null;
|
|
|
|
function chargerZbar() {
|
|
if (window.zbar) return Promise.resolve();
|
|
|
|
// zbar.js n'est pas un module ES : il pose window.zbar. D'où l'injection de balise
|
|
// plutôt qu'un import().
|
|
chargementZbar ??= new Promise((resolve, reject) => {
|
|
const balise = document.createElement('script');
|
|
balise.src = CHEMIN_ZBAR;
|
|
balise.onload = resolve;
|
|
balise.onerror = () => reject(new Error('zbar.js introuvable'));
|
|
document.head.appendChild(balise);
|
|
});
|
|
|
|
return chargementZbar;
|
|
}
|
|
|
|
// getUserMedia exige un contexte sécurisé : HTTPS, ou localhost.
|
|
// Une IP de réseau local (192.168.x.x) en http NE l'est PAS — piège classique en dev.
|
|
export function diagnostic() {
|
|
if (!window.isSecureContext) return 'contexte-non-securise';
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) return 'api-absente';
|
|
return 'ok';
|
|
}
|
|
|
|
// Renvoie un code de statut plutôt que de laisser remonter l'exception : une DOMException
|
|
// traversée par le pont JS→C# perd son `name` (mesuré : C# ne reçoit que « Permission denied
|
|
// undefined »), or c'est précisément ce nom qui dit à l'utilisateur ce qui lui manque.
|
|
export async function demarrer(element) {
|
|
video = element;
|
|
try {
|
|
// facingMode "environment" = caméra arrière : on scanne un livre tenu devant soi.
|
|
// "ideal" et non "exact" pour ne pas échouer sur un PC qui n'a qu'une webcam frontale.
|
|
flux = await navigator.mediaDevices.getUserMedia({
|
|
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
|
|
audio: false,
|
|
});
|
|
} catch (e) {
|
|
switch (e.name) {
|
|
case 'NotAllowedError':
|
|
case 'SecurityError':
|
|
return 'permission-refusee';
|
|
case 'NotFoundError':
|
|
case 'DevicesNotFoundError':
|
|
case 'OverconstrainedError':
|
|
return 'aucune-camera';
|
|
case 'NotReadableError':
|
|
case 'TrackStartError':
|
|
return 'camera-occupee';
|
|
default:
|
|
return 'erreur:' + (e.name || 'Error') + ' — ' + (e.message || '');
|
|
}
|
|
}
|
|
|
|
try {
|
|
await chargerZbar();
|
|
scanner = await window.zbar.ZBarScanner.create();
|
|
} catch (e) {
|
|
return 'erreur:Decodeur — ' + (e.message || 'zbar indisponible');
|
|
}
|
|
|
|
video.srcObject = flux;
|
|
video.setAttribute('playsinline', '');
|
|
await video.play();
|
|
canvas = document.createElement('canvas');
|
|
ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
return 'ok';
|
|
}
|
|
|
|
// Capture une image et la décode. Renvoie le premier code lu, ou null.
|
|
//
|
|
// ⚠️ L'image ENTIÈRE est analysée, contrairement à la version ZXing.Net qui n'envoyait que la
|
|
// bande centrale (45 % de la hauteur) pour limiter ce qui traversait le pont. Ce recadrage était
|
|
// l'une des deux causes possibles des codes non lus ; le décodage étant maintenant côté JS, il
|
|
// n'a plus de raison d'être.
|
|
export async function decoder(largeurMax) {
|
|
if (!video || !ctx || !scanner || video.readyState < 2 || !video.videoWidth) return null;
|
|
|
|
const echelle = Math.min(1, largeurMax / video.videoWidth);
|
|
const l = Math.round(video.videoWidth * echelle);
|
|
const h = Math.round(video.videoHeight * echelle);
|
|
if (canvas.width !== l || canvas.height !== h) { canvas.width = l; canvas.height = h; }
|
|
ctx.drawImage(video, 0, 0, l, h);
|
|
|
|
const symboles = await window.zbar.scanImageData(ctx.getImageData(0, 0, l, h), scanner);
|
|
if (!symboles || symboles.length === 0) return null;
|
|
|
|
// Les add-ons EAN-2 (numéro de parution) et EAN-5 (prix) sont imprimés à côté du code
|
|
// principal et remontent comme des symboles distincts : ils ne sont pas ce qu'on cherche.
|
|
const principal = symboles.find(s => {
|
|
const type = window.zbar.ZBarSymbolType[s.type];
|
|
return type !== 'ZBAR_EAN2' && type !== 'ZBAR_EAN5';
|
|
});
|
|
|
|
return principal ? principal.decode() : null;
|
|
}
|
|
|
|
// Libérer la caméra : sinon le voyant reste allumé et la batterie se vide,
|
|
// ce que l'utilisateur perçoit — à raison — comme un dysfonctionnement.
|
|
export function arreter() {
|
|
if (flux) { flux.getTracks().forEach(t => t.stop()); flux = null; }
|
|
if (video) { video.srcObject = null; video = null; }
|
|
if (scanner) { scanner.destroy(); scanner = null; }
|
|
canvas = null; ctx = null;
|
|
}
|