MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// 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();
|
||||
|
||||
// ⚠️ L'add-on EAN-2 est DÉSACTIVÉ par défaut dans zbar : sans cette ligne il n'est pas
|
||||
// seulement filtré, il n'est jamais produit. Mesuré le 2026-08-19 sur un EAN-13 + EAN-2
|
||||
// rendus en canvas — scanner par défaut : un seul symbole ; EAN-2 activé : deux.
|
||||
// C'est lui qui porte le numéro de parution d'un périodique.
|
||||
// L'EAN-5 reste désactivé : il porte un PRIX, dont on n'a rien à faire, et un symbole
|
||||
// de moins est un symbole de moins à écarter.
|
||||
scanner.setConfig(
|
||||
window.zbar.ZBarSymbolType.ZBAR_EAN2, window.zbar.ZBarConfigType.ZBAR_CFG_ENABLE, 1);
|
||||
} 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 { code, parution }, ou null si rien n'est lu.
|
||||
//
|
||||
// ⚠️ 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;
|
||||
|
||||
const typeDe = s => window.zbar.ZBarSymbolType[s.type];
|
||||
|
||||
// Les add-ons EAN-2 et EAN-5 sont imprimés À CÔTÉ du code principal et remontent comme des
|
||||
// symboles distincts. Le code du livre — ou de la revue — n'est jamais l'un d'eux.
|
||||
const principal = symboles.find(s => typeDe(s) !== 'ZBAR_EAN2' && typeDe(s) !== 'ZBAR_EAN5');
|
||||
if (!principal) return null;
|
||||
|
||||
// L'EAN-2 porte le numéro de parution d'un périodique ; l'EAN-5 porte un PRIX et n'a rien à
|
||||
// faire ici. On ne remonte donc que le premier, et seulement pour information : c'est
|
||||
// l'écran qui décidera d'en faire quelque chose, et l'utilisateur qui confirmera.
|
||||
const ean2 = symboles.find(s => typeDe(s) === 'ZBAR_EAN2');
|
||||
|
||||
return { code: principal.decode(), parution: ean2 ? ean2.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;
|
||||
}
|
||||
Reference in New Issue
Block a user