Ajouter le scan camera du code-barres ISBN

Le decodage EAN-13/EAN-8 est fait en C# par ZXing.Net (IsbnScanner), conformement
a la decision actee dans CLAUDE.md : le JavaScript ne fournit que les pixels de la
camera, aucune logique de decodage ne lui est confiee.

Le scan se greffe en amont du flux d'ajout par ISBN existant : des qu'un code est
lu et reconnu comme ISBN-13 valide, la recherche de notices est declenchee sans
que l'utilisateur ait a retaper quoi que ce soit. La saisie manuelle reste
accessible en permanence, y compris quand la camera echoue.

Points traites explicitement :
- camera arriere par defaut (facingMode "environment", en "ideal" pour ne pas
  echouer sur un PC sans camera arriere) ;
- liberation de la camera a l'annulation, au succes et au demontage du composant ;
- cadence limitee a ~12 images/s et decodage restreint a la bande centrale
  reduite a 640 px de large : inutile de decoder chaque frame en pleine taille ;
- messages distincts pour permission refusee, absence de camera, camera occupee
  et contexte non securise (getUserMedia exige HTTPS ou localhost : une IP de
  reseau local en http ne marchera jamais, c'est une source de confusion garantie).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-17 23:32:05 +02:00
co-authored by Claude Opus 5
parent b619de29c7
commit f7ad9582f6
9 changed files with 585 additions and 2 deletions
+33
View File
@@ -449,3 +449,36 @@ body {
flex: 0 0 auto;
}
}
/* --- Scan du code-barres --- */
.scanner-cadre {
position: relative;
background: #000;
border-radius: 8px;
overflow: hidden;
aspect-ratio: 4 / 3;
}
.scanner-video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Le viseur matérialise la bande réellement décodée : viser ailleurs ne sert à rien. */
.scanner-viseur {
position: absolute;
left: 5%;
right: 5%;
top: 27.5%;
height: 45%;
border: 2px solid rgba(255, 255, 255, 0.85);
border-radius: 6px;
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.35);
}
.scanner-mesure {
font-variant-numeric: tabular-nums;
}
@@ -0,0 +1,61 @@
// Rôle unique de ce fichier : ouvrir la caméra et fournir des pixels à C#.
// AUCUN décodage ici — il est fait par ZXing.Net côté C# (décision actée dans CLAUDE.md).
// getUserMedia et canvas/getImageData n'ont pas d'équivalent C#, d'où ces quelques lignes.
let flux = null;
let video = null;
let canvas = null;
let ctx = null;
let derniersPixels = null;
// 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';
}
export async function demarrer(element) {
video = element;
// 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,
});
video.srcObject = flux;
video.setAttribute('playsinline', '');
await video.play();
canvas = document.createElement('canvas');
ctx = canvas.getContext('2d', { willReadFrequently: true });
}
// Capture une frame et la garde en mémoire. Renvoie [largeur, hauteur], ou null si
// la vidéo n'est pas encore prête. Les pixels se récupèrent ensuite via pixels().
export function capturer(largeurMax, partHauteur) {
if (!video || !ctx || 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);
// On ne renvoie que la bande centrale, celle que l'utilisateur vise : moins de pixels
// à transférer et à décoder, donc moins de travail par frame côté C#.
const hb = Math.max(1, Math.round(h * partHauteur));
const y = Math.round((h - hb) / 2);
derniersPixels = new Uint8Array(ctx.getImageData(0, y, l, hb).data.buffer);
return [l, hb];
}
export function pixels() {
return derniersPixels;
}
// 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; }
canvas = null; ctx = null; derniersPixels = null;
}