Basculer le décodage des codes-barres de ZXing.Net vers zbar

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>
This commit is contained in:
mathieu
2026-08-19 00:51:56 +02:00
co-authored by Claude Opus 5
parent 718df2c986
commit 278d6579f7
8 changed files with 269 additions and 438 deletions
+105 -79
View File
@@ -45,104 +45,130 @@ Application self-hosted de gestion de bibliothèque personnelle, à héberger su
| AOT WebAssembly | **Désactivé par défaut**, à réévaluer après mesure | Le mode interprété devrait suffire (~5-10 ms/frame estimés) ; ne pas payer le coût de build avant d'avoir constaté un problème | | AOT WebAssembly | **Désactivé par défaut**, à réévaluer après mesure | Le mode interprété devrait suffire (~5-10 ms/frame estimés) ; ne pas payer le coût de build avant d'avoir constaté un problème |
| Hébergement | YunoHost, installation **native** (pas Docker) | YunoHost déconseille Docker pour ses apps (moins fiable, plus lourd) ; installation native = meilleures perfs sur petit matériel | | Hébergement | YunoHost, installation **native** (pas Docker) | YunoHost déconseille Docker pour ses apps (moins fiable, plus lourd) ; installation native = meilleures perfs sur petit matériel |
| Packaging YunoHost | S'inspirer de [`radarr_ynh`](https://github.com/YunoHost-Apps/radarr_ynh) | Radarr est aussi en .NET, packagé sans Docker sur YunoHost. Leur `manifest.toml` montre un déploiement **self-contained** (`dotnet publish -r linux-x64 --self-contained`), donc pas besoin d'installer `dotnet-runtime` via apt côté serveur — le binaire embarque son propre runtime | | Packaging YunoHost | S'inspirer de [`radarr_ynh`](https://github.com/YunoHost-Apps/radarr_ynh) | Radarr est aussi en .NET, packagé sans Docker sur YunoHost. Leur `manifest.toml` montre un déploiement **self-contained** (`dotnet publish -r linux-x64 --self-contained`), donc pas besoin d'installer `dotnet-runtime` via apt côté serveur — le binaire embarque son propre runtime |
| Scan ISBN | **ZXing.Net** (C#, Apache 2.0) exécuté dans le WASM ; le JS ne fournit que les pixels caméra | Décodage en C#, réutilisable hors navigateur si le projet évolue en scanner de bibliothèque. Voir la section dédiée ci-dessous | | Scan ISBN | **zbar** (LGPL-2.1) compilé en WebAssembly, via les assets du paquet `ZBar.Blazor` ; notre JS garde la caméra | ZXing.Net **ne lisait pas** des codes-barres que zbar lit sur le même livre (2026-08-19). Plus léger de surcroît. Voir la section dédiée ci-dessous |
| Consultation hors-ligne | **Instantanés JSON en IndexedDB**, lecture seule, implémenté le 2026-08-18 | Le besoin est de **consulter la bibliothèque existante** sans réseau, pas d'enrichir de nouveaux livres. Voir « Stratégie hors-ligne » | | Consultation hors-ligne | **Instantanés JSON en IndexedDB**, lecture seule, implémenté le 2026-08-18 | Le besoin est de **consulter la bibliothèque existante** sans réseau, pas d'enrichir de nouveaux livres. Voir « Stratégie hors-ligne » |
## Scan du code-barres — ZXing.Net (décision actée) ## Scan du code-barres — zbar (décision actée le 2026-08-19)
Le décodage EAN-13 se fait en **C# avec [ZXing.Net](https://www.nuget.org/packages/ZXing.Net)** (`micjahn`, Apache 2.0), et non avec une bibliothèque JS type `html5-qrcode`. Le décodage EAN-13 se fait par **[zbar](https://github.com/mchehab/zbar) compilé en
WebAssembly**, appelé depuis `wwwroot/js/scanner-camera.js`. Les assets viennent du paquet
NuGet [`ZBar.Blazor`](https://www.nuget.org/packages/ZBar.Blazor) (LGPL-2.1).
### Pourquoi ### Pourquoi ZXing.Net a été abandonné
- **Réutilisable hors navigateur.** Si le projet évolue vers un scanner de bibliothèque (app native, scan en masse, décodage d'une photo côté serveur), le code de décodage se transpose tel quel. Une bibliothèque JS serait à réécrire intégralement. **Constat d'usage** : l'ISBN `9782846391009` n'était pas décodé par l'application, alors qu'il
- **Un seul langage**, cohérent avec le reste de la stack. l'était par une application d'essai utilisant zbar, **sur le même livre et le même appareil**.
- L'argument « offline » n'entre **pas** en compte ici : `html5-qrcode` fonctionne aussi hors-ligne (fichier JS servi par la PWA, aucun appel réseau). Ce n'est pas un critère de départage.
### Mesures réelles (validées sur .NET 10, publish Blazor WASM OK) ⚠️ **Le raisonnement qui avait écarté cette piste était faux, et la faute est instructive.**
Ce fichier concluait que « le décodeur n'y est pour rien » à partir de mesures de **vitesse**
(4-6 ms/frame en WASM interprété). Or **la vitesse ne dit rien du taux de réussite** : un
décodeur peut être rapide et rater. Les mesures ne pouvaient pas soutenir cette conclusion.
| Mesure | Résultat | Ne jamais reconduire ce raccourci : pour juger un décodeur, il faut mesurer **ce qu'il lit**,
|---|---| pas ce qu'il coûte.
| Décodage EAN-13 propre (380×160) | 0,04 ms/frame |
| **Pire cas** : frame 640×480 bruitée sans code-barres (échec) | 0,53 ms/frame |
| Poids de l'assembly `zxing.wasm` seul | +192 Ko (brotli) |
Le pire cas est le chiffre qui gouverne le framerate : la majorité des frames caméra ne contiennent pas de code-barres lisible, et c'est l'échec de décodage qui coûte le plus cher. ### Ce qui est établi, et ce qui ne l'est pas
### Mesures en WASM réel — faites le 2026-08-18, phase scan **Établi** : zbar lit ce code-barres, ZXing.Net ne le lit pas, dans les mêmes conditions.
L'estimation « ~5-10 ms/frame en interprété » ci-dessus n'était qu'une extrapolation. Elle a **Non établi** : que le moteur soit *seul* en cause. L'ancienne implémentation ne faisait pas que
été **vérifiée dans un vrai navigateur** (Chromium 148, x86_64 de bureau), sur le publish décoder, elle **recadrait** sur la bande centrale (45 % de la hauteur) après réduction à 640 px,
`Release` du client, en appelant le décodeur depuis la console via `BancEssaiScan` : pour limiter ce qui traversait le pont JS→C#. Ce recadrage était la seconde cause possible.
La bascule l'a supprimé — l'image entière est désormais analysée, à 960 px de large — donc la
question ne se pose plus.
| Configuration | Pire cas 640×288 (bande visée) | Pire cas 640×480 | ### Ce que la bascule change dans le code
| | Avant (ZXing.Net) | Après (zbar) |
|---|---|---| |---|---|---|
| Publish `Release`, interprété | **4,4 6,4 ms/frame** | 6,7 7,8 ms/frame | | Décodage | C#, `IsbnScanner.TryDecode` | JS, `window.zbar.scanImageData` |
| Build `Debug`, interprété | 17,7 ms/frame | 28,7 ms/frame | | Pixels sur le pont JS→C# | **un `byte[]` par frame** | aucun — seule la valeur décodée passe |
| Zone analysée | bande centrale, 640 px | image entière, 960 px |
| Caméra | `scanner-camera.js` | `scanner-camera.js`, **inchangé** |
**L'estimation était bonne** : le mode interprété tient largement les 10-15 fps visés (une `IsbnScanner.cs`, `BancEssaiScan.cs` et `IsbnScannerTests.cs` ont disparu.
frame toutes les 80 ms n'utilise que ~6 % du budget). **Aucune raison d'activer l'AOT**
il reste à confirmer sur un téléphone, sensiblement plus lent qu'un x86_64 de bureau.
⚠️ Ne jamais juger la fluidité sur un build `Debug` : il est **4 à 5× plus lent** que le ⚠️ **Coût assumé : six tests de décodage en moins.** Ils s'exécutaient en C# sur des images
`Release`, de quoi conclure à tort qu'il faut l'AOT. générées ; le décodeur étant maintenant en JS, ils n'ont plus d'équivalent dans xUnit. Le
décodage a été vérifié dans le navigateur, sur un EAN-13 rendu en canvas — `9782846391009`
ressort bien en `ZBAR_EAN13`. C'est une vérification, pas un garde-fou permanent.
### ⚠️ Le surcoût de payload réel est bien supérieur à 192 Ko ### ⚠️ On n'utilise PAS le composant `ZBarCamera` du paquet — et il ne faut pas y revenir
Mesuré par différence entre deux publish `Release` complets (somme brotli de `_framework`) : Son `camera.js` ouvre la caméra lui-même et **avale les erreurs** :
**+351 Ko** au total, dont ~4,7 Ko de code applicatif. Le coût imputable à ZXing.Net est donc
d'environ **+346 Ko brotli**, soit **1,8× le poids de son propre assembly**. Le surplus vient
des assemblies BCL que le trimmer ne peut plus retirer :
| Assembly | Delta brotli | ```js
|---|---| navigator.mediaDevices.getUserMedia(constraints).then(...).catch(function (error) {
| `zxing.wasm` | +192 495 o | console.log(error); // et c'est tout
| `System.Text.RegularExpressions` | +91 237 o (de 7 Ko à 98 Ko : ZXing utilise Regex, tout le moteur reste) | });
| `System.Runtime.Numerics` | +30 701 o (nouveau) |
| `System.Private.CoreLib` | +17 586 o |
| divers (`Threading`, `Collections`, `InteropServices`…) | ~14 Ko |
Ne pas reprendre « +192 Ko » comme coût du scan : c'est le poids de l'assembly, pas celui
de la fonctionnalité.
### Pièges à connaître
- **Le JS interop ne disparaît pas.** `getUserMedia` et `<canvas>`/`getImageData` sont des API web sans équivalent C#. Prévoir ~30 lignes de JS maison dont le seul rôle est de pousser un `byte[]` vers C#. Toute la logique de décodage reste en C#.
- **Ne pas perdre de temps à essayer de réduire la taille via un reader ciblé.** Remplacer `MultiFormatReader` par `EAN13Reader` pour aider le trimmer **ne change rien** : mesuré à 192 495 octets à l'octet près dans les deux cas. ZXing.Net n'est pas trim-friendly.
- `RGBLuminanceSource` accepte directement le buffer RGBA du canvas (`BitmapFormat.RGBA32`) — **aucune bibliothèque d'image nécessaire** (pas de SkiaSharp ni ImageSharp).
- Le scan caméra exige **HTTPS** (garanti par YunoHost en prod ; en dev, `localhost` est considéré comme sûr). Corollaire vérifié : tester le scan depuis un téléphone en pointant l'IP locale du PC (`http://192.168.x.x`) **échouera toujours** — ce n'est pas un contexte sécurisé. Le composant détecte ce cas et le dit explicitement.
- **Une `DOMException` perd son `name` en traversant le pont JS→C#** : `getUserMedia` refusé remonte en C# sous la forme « Permission denied undefined », sans `NotAllowedError`. Or c'est ce nom qui distingue « permission refusée » de « pas de caméra » de « caméra occupée ». Le JS doit donc **attraper l'erreur et renvoyer un code de statut** ; parser le message côté C# ne marche pas.
- **L'import du module JS se fait sur un chemin nu** (`./js/scanner-camera.js`). Il a longtemps porté une chaîne de requête `?m=1` : l'import map généré par Blazor réécrivait le chemin vers un nom empreinté que l'API hôte ne servait pas (elle utilise `UseStaticFiles`, qui ignore les points d'entrée empreintés), et l'import partait en 404. Depuis que les empreintes WASM sont désactivées (voir « Empreintes WASM désactivées »), **il n'y a plus d'import map du tout** et le contournement a été retiré. Vérifié en développement et sur le publish self-contained : le module se charge dans les deux cas.
### Squelette validé
```csharp
using ZXing;
using ZXing.Common;
public static class IsbnScanner
{
static readonly MultiFormatReader Reader = new()
{
Hints = new Dictionary<DecodeHintType, object>
{
[DecodeHintType.POSSIBLE_FORMATS] = new List<BarcodeFormat>
{
BarcodeFormat.EAN_13, BarcodeFormat.EAN_8,
},
[DecodeHintType.TRY_HARDER] = true,
},
};
/// rgba : buffer brut issu de ctx.getImageData(...).data
public static string? TryDecode(byte[] rgba, int width, int height)
{
var source = new RGBLuminanceSource(
rgba, width, height, RGBLuminanceSource.BitmapFormat.RGBA32);
return Reader.decode(new BinaryBitmap(new HybridBinarizer(source)))?.Text;
}
}
``` ```
On perdrait d'un coup :
- les messages qui distinguent **permission refusée**, **aucune caméra**, **caméra occupée** et
**contexte non sécurisé** — ce que ce fichier documente comme durement acquis (une
`DOMException` perd son `name` en traversant le pont, d'où les codes de statut) ;
- `facingMode: { ideal: 'environment' }`, donc la **caméra arrière** sur téléphone : le composant
demande `{ video: true }`, c'est-à-dire la caméra frontale par défaut ;
- l'indication de résolution `1280×720`.
Le paquet est donc référencé **pour ses assets statiques seulement** (`zbar.js`, `zbar.wasm`).
Vérifié après bascule : caméra refusée → « L'accès à la caméra a été refusé… », message intact.
### `zbar.wasm` est chargé à la demande, pas au démarrage
`scanner-camera.js` injecte `_content/ZBar.Blazor/zbar.js` **à la première ouverture du
scanner**. Personne ne télécharge 139 Ko pour consulter sa bibliothèque.
⚠️ `zbar.js` **n'est pas un module ES** : il pose `window.zbar`. D'où l'injection d'une balise
`<script>` plutôt qu'un `import()`.
### Poids : mesuré par comparaison de deux publish complets
Somme brotli, `MaBibli.Api` publié en `Release`, avant et après :
| | ZXing.Net | zbar |
|---|---|---|
| `_framework` (téléchargé au démarrage) | 3 281 558 o | **2 978 700 o** |
| `_content` (à la demande) | 0 | 146 841 o |
| **Total** | 3 281 558 o | **3 125 541 o** |
**152 Kio au total, et 296 Kio au démarrage.** L'écart ne vient pas du moteur mais de la
**traîne** que ZXing.Net imposait au trimmer :
| Assembly | ZXing.Net | zbar |
|---|---|---|
| `zxing.wasm` | 192 495 o | absent |
| `System.Text.RegularExpressions` | 98 374 o | **7 137 o** |
| `System.Runtime.Numerics` | 30 701 o | absent |
| `ZBar.Blazor` (assembly .NET, inutilisé mais embarqué) | absent | 21 232 o |
`System.Text.RegularExpressions` retombe à sa taille trimmée parce que plus rien n'utilise
`Regex` : c'est ZXing qui l'imposait. ⚠️ L'assembly `ZBar.Blazor` est embarqué bien qu'aucun de
ses types ne soit utilisé — 21 Ko dont on se passerait, mais le paquet reste la manière propre
d'obtenir `zbar.wasm` et sa licence.
**Leçon confirmée, dans les deux sens** : le coût d'une bibliothèque n'est pas le poids de son
assembly. Toujours comparer deux publish.
### Ce qui reste vrai de l'ancienne section
- Le scan caméra exige **HTTPS** (garanti par YunoHost ; `localhost` est considéré comme sûr).
Tester depuis un téléphone en pointant l'IP locale du PC (`http://192.168.x.x`) **échouera
toujours** — ce n'est pas un contexte sécurisé, et le composant le dit.
- **Une `DOMException` perd son `name` en traversant le pont JS→C#.** Le JS doit renvoyer un
code de statut ; parser le message côté C# ne marche pas.
- **L'import du module se fait sur un chemin nu** (`./js/scanner-camera.js`), les empreintes WASM
étant désactivées (voir « Empreintes WASM désactivées »).
### Ce que zbar apporte en plus
- Il remonte le **format** du symbole (`ZBAR_EAN13`, `ZBAR_ISBN13`, `ZBAR_EAN5`…), là où nous
déduisons le type du préfixe.
- Il décode les **add-ons EAN-2 (numéro de parution) et EAN-5 (prix)** imprimés à côté du code
principal. ⚠️ `decoder()` les **écarte** : ce ne sont pas le code du livre. L'EAN-2 deviendra
utile le jour où les périodiques seront catalogués.
## Saisie d'un code-barres — décisions actées le 2026-08-18 (2ᵉ série) ## Saisie d'un code-barres — décisions actées le 2026-08-18 (2ᵉ série)
### La douchette USB est le vrai remède au scan raté sur PC ### La douchette USB est le vrai remède au scan raté sur PC
+72 -123
View File
@@ -59,101 +59,22 @@ ce qui suit est la matière brute, classée par sujet, avec ce que le diagnostic
### Le scan caméra rate souvent sur ordinateur ### Le scan caméra rate souvent sur ordinateur
**La douchette USB est faite** (voir `CLAUDE.md`, « La douchette USB est le vrai remède ») : **Deux remèdes appliqués**, tous deux dans `CLAUDE.md` : la **douchette USB** (champ focalisé,
champ focalisé à l'ouverture, validation sur `Entrée`. Reste à confirmer en usage réel qu'elle validation sur `Entrée`) et surtout la **bascule de ZXing.Net vers zbar** — c'était bien le
suffit. décodeur, contrairement à ce qu'on avait conclu de mesures de vitesse.
Si elle ne suffit pas, pistes non traitées : choisir la caméra quand il y en a plusieurs, Reste à confirmer en usage réel. Si cela ne suffisait toujours pas : choisir la caméra quand il
demander une résolution plus haute, exposer un curseur de zoom/torche là où l'API le permet, y en a plusieurs, exposer un curseur de zoom/torche là où l'API le permet, et laisser **déposer
et laisser **déposer une photo** du code-barres à décoder (ZXing lit un fichier aussi bien une photo** du code-barres à décoder.
qu'une frame).
### ISBN `9782846391009` : le scan n'aboutit pas ### ISBN `9782846391009` : résolu — c'était le décodeur
**La donnée existe pourtant côté BnF** — vérifié le 2026-08-18 : Le livre existe bien à la BnF : l'échec était **au décodage de l'image**. Confirmé le
2026-08-19 — zbar lit ce code-barres, ZXing.Net ne le lisait pas, sur le même appareil et le
même livre. La bascule est faite (voir `CLAUDE.md`).
``` ⚠️ **À confirmer avec le livre en main** : la vérification faite jusqu'ici porte sur un EAN-13
bib.isbn all "9782846391009" → 1 notice rendu en canvas, pas sur une image de caméra.
dc:title = La thérapie émotivo-rationnelle / Albert Ellis et Robert A. Harper
dc:creator = Ellis, Albert (1913-2007). Auteur du texte
dc:creator = Harper, Robert A. (1915-2004). Auteur du texte
(la forme ISBN-10 « 2846391009 » ne renvoie rien : ici c'est bien l'ISBN-13 qui indexe)
```
L'échec est donc **au décodage de l'image**, pas au lookup. Rejoint le point ci-dessus.
⚠️ À reproduire en saisissant l'ISBN à la main avant de conclure : si la saisie manuelle échoue
aussi, le défaut est ailleurs et ce diagnostic tombe.
## Choix de la bibliothèque de lecture de code-barres — à trancher
**Constat qui rouvre le sujet, 2026-08-19.** L'ISBN `9782846391009` n'est pas décodé par
l'application, alors qu'il l'est par une application d'essai (`~/Code/TestBarcode`) sur le
**même appareil et le même livre**, avec **ZBar**.
⚠️ **Le raisonnement qui écartait ce sujet était faux.** `CLAUDE.md` conclut que « le décodeur
n'y est pour rien » à partir de mesures de **vitesse** (4-6 ms/frame en WASM interprété). Or la
vitesse ne dit **rien du taux de réussite** : un décodeur peut être rapide et rater. Les mesures
existantes ne pouvaient donc pas soutenir cette conclusion, et l'essai la contredit.
### Ce que l'essai établit, et ce qu'il n'établit pas
**Établi** : ZBar lit ce code-barres, ZXing.Net (portage C#) ne le lit pas, dans les mêmes
conditions physiques.
**Non établi** : que le moteur soit seul en cause. Notre implémentation ne fait pas que décoder,
elle **prétraite** — réduction à 640 px de large, puis **recadrage sur la bande centrale**
(`PartHauteur = 0,45`). ZBar, lui, analyse l'image entière. Deux causes candidates cohabitent
donc, et l'essai ne les sépare pas :
1. le moteur (ZXing.Net est un portage d'une version ancienne de ZXing, réputé moins robuste que
`zbar` ou que `zxing-js`) ;
2. notre prétraitement (bande trop étroite, ou résolution insuffisante pour la largeur de module
à la distance de prise de vue).
Adopter ZBar rendrait la seconde question sans objet : le composant gère lui-même la caméra et
l'image.
### Le poids : mesuré, et à l'inverse de ce qu'on croyait
Somme brotli réelle du publish `Release` de l'application d'essai :
| Fichier | brotli |
|---|---|
| `zbar.wasm` (moteur C compilé) | 138 938 o |
| `ZBar.Blazor.<empreinte>.wasm` (assembly .NET) | 21 232 o |
| `zbar.js` | 5 247 o |
| `camera.js` + `image.js` + `scanner.js` | 2 457 o |
| CSS isolé | 199 o |
| **Total ZBar** | **≈ 168 Ko** |
À comparer au coût de ZXing.Net **déjà documenté dans `CLAUDE.md`** : **≈ 346 Ko brotli**, dont
seulement 192 Ko d'assembly — le reste étant les assemblies BCL que le trimmer ne peut plus
retirer (`System.Text.RegularExpressions` passant de 7 à 98 Ko parce que ZXing utilise Regex,
plus `System.Runtime.Numerics`).
Vérifié dans le publish d'essai : `System.Text.RegularExpressions` y pèse **7 137 o**, et
`System.Runtime.Numerics` est **absent**. ZBar ne traîne pas cette queue.
**Le remplacement allégerait donc l'application d'environ 180 Ko brotli**, au lieu de l'alourdir.
⚠️ Chiffre à confirmer par une comparaison de deux publish de *notre* application, comme l'a été
celui de ZXing.Net : le coût d'une bibliothèque n'est pas le poids de son assembly.
### Ce qu'on perdrait
- **Le décodage quitte le C#.** C'est l'argument qui avait fait retenir ZXing.Net : « réutilisable
hors navigateur si le projet évolue en scanner de bibliothèque ». Cet usage est **hypothétique
et n'a jamais été exercé** ; il se paie aujourd'hui en codes-barres non lus.
- **`IsbnScanner`, `BancEssaiScan`, `ScannerCodeBarres` et `wwwroot/js/scanner-camera.js`**
disparaîtraient au profit du composant `ZBarCamera`.
- **Une dépendance jeune** : `ZBar.Blazor` 1.1.0. À regarder avant de s'engager (maintenance,
licence, taille de la communauté).
### Ce qu'on gagnerait, au-delà du code lu
- **Le format est remonté** (`ISBN_13` vs `EAN_13`), là où nous le déduisons du préfixe.
- **Les add-ons EAN-5 (prix) et EAN-2 (numéro) sont décodés** et rattachables au code principal —
l'EAN-2 est précisément ce qui accompagne les périodiques en `977`.
- **Plus de pipeline image maison** à entretenir (capture, réduction, recadrage, `RGBLuminanceSource`).
--- ---
@@ -181,28 +102,23 @@ quand même ? »), plutôt que l'index unique qui interdirait le second exemplai
d'envies, elle, refuse — mais elle décrit une envie, pas un objet, et deux exemplaires d'une d'envies, elle, refuse — mais elle décrit une envie, pas un objet, et deux exemplaires d'une
envie n'ont pas de sens. envie n'ont pas de sens.
## À l'ajout d'un livre, retirer l'envie correspondante ## À l'ajout d'un livre, signaler l'envie correspondante
Demandé : si le livre ajouté figure dans la liste d'envies, l'en retirer. **Décidé le 2026-08-19 : on ne supprime rien, on signale.** La liste d'envies marque l'entrée
« déjà au catalogue ».
⚠️ **La difficulté est la portée, pas le rapprochement.** Le catalogue est **commun**, la liste Pourquoi pas la suppression, qui était la demande initiale :
d'envies est **personnelle** — ce sont les deux portées que `CLAUDE.md` oppose explicitement.
« Retirer l'envie » n'a donc pas une seule réponse :
- **la sienne uniquement** : simple, sûr, ne franchit aucune frontière. Mais si Camille souhaitait - le catalogue est **commun**, la liste d'envies **personnelle**. Supprimer l'envie d'un autre
le livre et que Mathieu l'ajoute, Camille garde une envie pour un livre que le foyer possède. modifierait sa liste en silence et lui ferait perdre sa note (« demandé à Noël ») ; l'API ne
- **celle de tout le monde** : cohérent avec « le livre est commun », mais **modifie sait pas — délibérément — écrire dans la liste d'autrui, et l'ouvrir serait une brèche dans un
silencieusement la liste d'un autre**, et lui fait perdre sa note (« demandé à Noël »). L'API invariant tenu partout ailleurs ;
ne sait pas — délibérément — écrire dans la liste d'autrui : ce serait une brèche à ouvrir - le rapprochement se fait par `CleOeuvre` + auteur, donc **faillible** (titre retraduit, tome,
dans un invariant tenu jusqu'ici. intégrale — voir `CLAUDE.md`). Une suppression fondée sur un rapprochement faux est
- **ne rien supprimer, mais signaler** : la liste d'envies marque l'entrée « déjà au catalogue ». irréversible ; un signalement se corrige d'un coup d'œil.
Non destructif, respecte la frontière, et l'écran de bibliographie sait déjà distinguer
`Possede` de `Souhaite`.
⚠️ Le rapprochement se ferait par `CleOeuvre` + auteur, avec les mêmes limites que la Reste à implémenter : le marquage dans `SouhaitDto`, calculé comme l'est déjà `Possede` dans la
bibliographie (titre retraduit, tome, intégrale — voir `CLAUDE.md`). Une suppression sur un bibliographie, et son rendu sur l'écran des envies.
rapprochement faillible est **irréversible** ; un signalement ne l'est pas. C'est un argument de
poids pour la troisième voie.
## Bandes dessinées et magazines ## Bandes dessinées et magazines
@@ -222,24 +138,57 @@ rien.
Champs propres à la BD (série, tome, scénariste/dessinateur) : à ne faire que si le besoin se Champs propres à la BD (série, tome, scénariste/dessinateur) : à ne faire que si le besoin se
confirme. Le modèle actuel met tous les auteurs dans `LivreAuteur` sans distinguer les rôles. confirme. Le modèle actuel met tous les auteurs dans `LivreAuteur` sans distinguer les rôles.
### Les magazines : cela rouvre une décision actée ### Les magazines : une fiche par revue, les numéros à l'intérieur
⚠️ `CLAUDE.md` (lot 1) écarte **délibérément** le catalogage des périodiques : « le modèle de ⚠️ Rouvre la décision du lot 1, qui écartait le catalogage des périodiques faute de modèle.
données n'a ni numéro ni date de parution, et les deux chiffres de parution du code ne sont pas Le modèle est maintenant choisi.
exploitables comme numéro fiable — douze numéros d'un même magazine partagent leur ISSN et
créeraient douze fiches identiques ». Le scan d'un code `977` nomme donc la revue et bascule sur
la saisie manuelle, **sans** l'ISBN.
Les supporter vraiment demande de lever exactement ce qui avait motivé le refus : **Décidé le 2026-08-19 : une fiche par REVUE**, et dans sa page, la sélection d'un numéro
précis. C'est exactement ce qui évite le défaut redouté — douze numéros d'un même magazine ne
font plus douze fiches identiques, puisqu'ils partagent la fiche de leur revue.
- un **ISSN** distinct de l'ISBN (le ranger dans `Livre.Isbn` casserait tout lookup ultérieur) ; Ce que cela demande :
- un **numéro** et une **date de parution**, seuls capables de distinguer deux exemplaires ;
- de décider si un magazine est **une fiche par numéro** ou **une fiche par revue avec des
numéros rattachés** — c'est le vrai choix de modélisation, et il change la migration ;
- de reprendre le flux `977` pour qu'il crée au lieu d'expliquer.
À trancher avant tout code. Le reste (filtres, prêts, hors-ligne) suit mécaniquement une fois le - un **ISSN** sur la fiche de revue, distinct de l'ISBN. ⚠️ Ranger un code `977` dans
modèle arrêté. `Livre.Isbn` casserait tout lookup ultérieur sur cette fiche ;
- une table de **numéros** rattachés à la revue, portant au minimum un numéro et une date de
parution ;
- reprendre le flux `977` : il nomme aujourd'hui la revue et bascule sur la saisie manuelle, il
devra créer ou compléter la fiche puis proposer d'ajouter le numéro scanné. ⚠️ Les deux
chiffres de parution du code-barres **ne sont pas un numéro fiable** ; l'add-on **EAN-2**, que
zbar sait maintenant décoder, est le bon candidat — à vérifier sur des magazines réels ;
- décider ce qu'un **prêt** signifie pour une revue : on prête un numéro, pas un abonnement.
## Sagas et cycles : plusieurs livres, un ordre de lecture
Demandé le 2026-08-19. Cas donné : [Drizzt Do'Urden](https://fr.wikipedia.org/wiki/Drizzt_Do%27Urden)
**plusieurs regroupements de plusieurs livres, à lire dans un ordre précis**. Vaut pour les
romans comme pour les BD.
C'est la première demande du projet qui porte sur une **relation entre livres** plutôt que sur
un livre. Ce que le cas Drizzt impose, et qu'un simple champ « série » ne couvrirait pas :
- **Deux niveaux.** *La Légende de Drizzt* regroupe plusieurs trilogies (*L'Elfe noir*,
*La Séquence de l'Icewind Dale*…) : un cycle contient des séries, qui contiennent des tomes.
Un champ texte plat perdrait ce regroupement.
- **L'ordre de lecture n'est pas l'ordre de publication.** *L'Elfe noir* est une préquelle écrite
après. C'est précisément l'information qu'on vient chercher : elle doit être **stockée**, pas
déduite de l'année.
- **Un livre peut appartenir à plusieurs regroupements** (une intégrale, une série, un cycle).
- **Ce qu'on possède est partiel.** L'intérêt de l'écran est de montrer les trous — « il vous
manque le tome 3 » — comme la bibliographie montre déjà ce qui manque d'un auteur.
Questions à trancher avant tout modèle :
- **D'où viennent les séries ?** La BnF ne les expose pas de façon exploitable en Dublin Core.
Saisie à la main, déduction depuis le titre (« Tome 3 »), ou source tierce ?
- **Portée** : commune au foyer, comme le catalogue, ou personnelle ? L'ordre de lecture est une
propriété de l'œuvre et non du lecteur — donc commune, a priori.
- **Les tomes non possédés** existent-ils en base, ou sont-ils seulement affichés ? S'ils
existent, ils ressemblent beaucoup à des envies, et il faudra dire comment les deux cohabitent.
⚠️ Ne pas confondre avec le regroupement d'œuvres de la bibliographie (`CleOeuvre`), qui réunit
les **rééditions d'un même titre**. Ici, ce sont des titres **différents** qui se suivent.
## Auteurs et bibliographie ## Auteurs et bibliographie
@@ -1,15 +1,16 @@
@* @*
Scan du code-barres ISBN par la caméra. Scan du code-barres ISBN par la caméra.
Le JS (wwwroot/js/scanner-camera.js) ne fait QUE fournir des pixels : tout le décodage Le décodage se fait dans wwwroot/js/scanner-camera.js, par zbar compilé en WebAssembly.
est en C# via ZXing.Net (IsbnScanner), conformément à la décision actée dans CLAUDE.md. ⚠️ Il était en C# (ZXing.Net) jusqu'au 2026-08-19 : voir CLAUDE.md, ZXing.Net ne lisait pas
des codes-barres que zbar lit sur le même livre. Ce composant ne voit plus passer un seul
pixel — seulement la valeur décodée.
On ne décode pas à chaque frame de la caméra : la cadence est volontairement limitée On ne décode pas à chaque frame de la caméra : la cadence est volontairement limitée
(voir IntervalleMs). 10-15 images/s suffisent largement pour scanner un livre tenu à la (voir IntervalleMs). 10-15 images/s suffisent largement pour scanner un livre tenu à la
main, et cela ménage la batterie du téléphone. main, et cela ménage la batterie du téléphone.
*@ *@
@using System.Diagnostics @using System.Diagnostics
@using MaBibli.Client.Scan
@using MaBibli.Shared.Isbn @using MaBibli.Shared.Isbn
@implements IAsyncDisposable @implements IAsyncDisposable
@inject IJSRuntime JS @inject IJSRuntime JS
@@ -44,9 +45,9 @@
@if (_framesDecodees > 0) @if (_framesDecodees > 0)
{ {
@* Mesure réelle en WASM interprété : CLAUDE.md ne dispose que d'une estimation @* Mesure sur l'appareil de l'utilisateur. ⚠️ Elle porte maintenant sur la capture,
(~5-10 ms/frame extrapolés du JIT x64). Cette ligne donne le chiffre constaté le décodage zbar ET l'aller-retour d'interopérabilité : elle n'est pas comparable
sur l'appareil de l'utilisateur, seul moyen de décider d'activer l'AOT. *@ aux chiffres ZXing.Net de CLAUDE.md, qui ne mesuraient que le décodage C#. *@
<p class="message-discret scanner-mesure"> <p class="message-discret scanner-mesure">
Décodage : @_msMoyen.ToString("0.0") ms/frame en moyenne (@_framesDecodees frames) Décodage : @_msMoyen.ToString("0.0") ms/frame en moyenne (@_framesDecodees frames)
</p> </p>
@@ -76,12 +77,13 @@
// ~12 images/s : bien assez pour scanner, et deux fois moins de travail que 25 fps. // ~12 images/s : bien assez pour scanner, et deux fois moins de travail que 25 fps.
private const int IntervalleMs = 80; private const int IntervalleMs = 80;
// La frame est réduite avant transfert : un 1280×720 en RGBA pèse 3,6 Mo à traverser // L'image est réduite avant décodage. Elle ne traverse plus le pont JS→C# — c'est zbar,
// le pont JS→C# à chaque itération. 640 px de large suffisent à lire un EAN-13. // côté JS, qui la lit — mais 960 px de large restent un bon compromis entre le détail
private const int LargeurMax = 640; // nécessaire à un EAN-13 dense et le temps de décodage.
//
// Seule la bande centrale est décodée : c'est là que l'utilisateur vise. // ⚠️ Plus large qu'avec ZXing.Net (640 px), et surtout l'image ENTIÈRE est analysée : le
private const double PartHauteur = 0.45; // recadrage sur la bande centrale n'existait que pour alléger le transfert de pixels.
private const int LargeurMax = 960;
private ElementReference _video; private ElementReference _video;
private IJSObjectReference? _module; private IJSObjectReference? _module;
@@ -165,16 +167,8 @@
return; return;
} }
var dimensions = await _module.InvokeAsync<int[]?>("capturer", LargeurMax, PartHauteur);
if (dimensions is null)
{
continue; // vidéo pas encore prête
}
var pixels = await _module.InvokeAsync<byte[]>("pixels");
var depart = Stopwatch.GetTimestamp(); var depart = Stopwatch.GetTimestamp();
var code = IsbnScanner.TryDecode(pixels, dimensions[0], dimensions[1]); var code = await _module.InvokeAsync<string?>("decoder", LargeurMax);
_totalMs += Stopwatch.GetElapsedTime(depart).TotalMilliseconds; _totalMs += Stopwatch.GetElapsedTime(depart).TotalMilliseconds;
_framesDecodees++; _framesDecodees++;
+14 -2
View File
@@ -18,8 +18,20 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.8" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.8" PrivateAssets="all" />
<!-- Décodage EAN-13 en C# (Apache 2.0). Le JS ne fournit que les pixels de la caméra. --> <!--
<PackageReference Include="ZXing.Net" Version="0.16.11" /> Décodage des codes-barres : moteur zbar (LGPL-2.1) compilé en WebAssembly.
⚠️ Le paquet est référencé pour ses ASSETS STATIQUES (_content/ZBar.Blazor/zbar.js et
zbar.wasm), pas pour ses composants Blazor. Son composant ZBarCamera ouvre lui-même la
caméra et AVALE les erreurs (« catch { console.log(error) } ») : on perdrait les messages
qui distinguent permission refusée, absence de caméra, caméra occupée et contexte non
sécurisé — précisément ce que CLAUDE.md documente comme durement acquis. On garde donc
notre scanner-camera.js et on n'emprunte que le décodeur.
Remplace ZXing.Net, qui ne lisait pas certains codes-barres pourtant lus par zbar sur le
même livre et le même appareil (voir CLAUDE.md).
-->
<PackageReference Include="ZBar.Blazor" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
-40
View File
@@ -1,40 +0,0 @@
using System.Diagnostics;
using Microsoft.JSInterop;
namespace MaBibli.Client.Scan;
/// <summary>
/// Mesure du coût réel du décodage <b>dans le navigateur</b>.
///
/// CLAUDE.md ne dispose que de chiffres JIT x64 (0,53 ms/frame dans le pire cas) et d'une
/// extrapolation non vérifiée pour le WASM interprété (~5-10 ms/frame). Ce banc d'essai
/// donne le chiffre constaté, sur la machine de l'utilisateur, sans caméra ni code-barres.
///
/// On mesure volontairement le <b>pire cas</b> — une frame de bruit sans code-barres —
/// car c'est l'échec de décodage qui coûte le plus cher, et la quasi-totalité des frames
/// d'une caméra sont dans ce cas. C'est donc lui qui gouverne le framerate atteignable.
///
/// Depuis la console du navigateur :
/// <code>await DotNet.invokeMethodAsync('MaBibli.Client', 'MesurerScan', 640, 288, 20)</code>
/// </summary>
public static class BancEssaiScan
{
[JSInvokable("MesurerScan")]
public static double MesurerPireCasMs(int largeur, int hauteur, int iterations)
{
var rgba = new byte[largeur * hauteur * 4];
new Random(1234).NextBytes(rgba);
// Un tour à blanc : la première exécution paie l'initialisation de ZXing,
// qui n'a rien à voir avec le coût par frame en régime établi.
IsbnScanner.TryDecode(rgba, largeur, hauteur);
var depart = Stopwatch.GetTimestamp();
for (var i = 0; i < iterations; i++)
{
IsbnScanner.TryDecode(rgba, largeur, hauteur);
}
return Stopwatch.GetElapsedTime(depart).TotalMilliseconds / iterations;
}
}
-53
View File
@@ -1,53 +0,0 @@
using ZXing;
using ZXing.Common;
namespace MaBibli.Client.Scan;
/// <summary>
/// Décodage d'un code-barres EAN-13 / EAN-8 à partir d'un buffer de pixels RGBA.
///
/// Décision actée dans CLAUDE.md : le décodage reste **entièrement en C#** (ZXing.Net),
/// le JavaScript ne sert qu'à fournir les pixels de la caméra. Cette classe ne dépend
/// donc ni du navigateur, ni de Blazor : elle est réutilisable telle quelle hors WASM
/// (app native, décodage d'une photo côté serveur, scan en masse).
/// </summary>
public static class IsbnScanner
{
// ZXing.Net n'est pas thread-safe sur ce lecteur, mais le WASM est mono-thread
// et la boucle de scan est séquentielle : une instance partagée suffit et évite
// de reconstruire les hints à chaque frame.
private static readonly MultiFormatReader Reader = new()
{
Hints = new Dictionary<DecodeHintType, object>
{
[DecodeHintType.POSSIBLE_FORMATS] = new List<BarcodeFormat>
{
BarcodeFormat.EAN_13, BarcodeFormat.EAN_8,
},
[DecodeHintType.TRY_HARDER] = true,
},
};
/// <summary>
/// Tente de décoder un code-barres. Renvoie le texte brut du code, ou <c>null</c>
/// si la frame n'en contient aucun de lisible — cas très majoritaire en usage réel.
/// </summary>
/// <param name="rgba">Buffer brut issu de <c>ctx.getImageData(...).data</c> (4 octets par pixel).</param>
public static string? TryDecode(byte[] rgba, int largeur, int hauteur)
{
ArgumentNullException.ThrowIfNull(rgba);
if (largeur <= 0 || hauteur <= 0 || rgba.Length < largeur * hauteur * 4)
{
return null;
}
// RGBLuminanceSource consomme directement le buffer du canvas :
// aucune bibliothèque d'image nécessaire (ni SkiaSharp, ni ImageSharp).
var source = new RGBLuminanceSource(
rgba, largeur, hauteur, RGBLuminanceSource.BitmapFormat.RGBA32);
// La version .NET de ZXing renvoie null sur échec (pas d'exception).
return Reader.decode(new BinaryBitmap(new HybridBinarizer(source)))?.Text;
}
}
+63 -18
View File
@@ -1,12 +1,41 @@
// Rôle unique de ce fichier : ouvrir la caméra et fournir des pixels à C#. // Rôle de ce fichier : ouvrir la caméra ET décoder les codes-barres avec zbar.
// 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. // ⚠️ 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 flux = null;
let video = null; let video = null;
let canvas = null; let canvas = null;
let ctx = null; let ctx = null;
let derniersPixels = 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. // 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. // Une IP de réseau local (192.168.x.x) en http NE l'est PAS — piège classique en dev.
@@ -44,6 +73,14 @@ export async function demarrer(element) {
return 'erreur:' + (e.name || 'Error') + ' — ' + (e.message || ''); 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.srcObject = flux;
video.setAttribute('playsinline', ''); video.setAttribute('playsinline', '');
await video.play(); await video.play();
@@ -52,25 +89,32 @@ export async function demarrer(element) {
return 'ok'; return 'ok';
} }
// Capture une frame et la garde en mémoire. Renvoie [largeur, hauteur], ou null si // Capture une image et la décode. Renvoie le premier code lu, ou null.
// la vidéo n'est pas encore prête. Les pixels se récupèrent ensuite via pixels(). //
export function capturer(largeurMax, partHauteur) { // ⚠️ L'image ENTIÈRE est analysée, contrairement à la version ZXing.Net qui n'envoyait que la
if (!video || !ctx || video.readyState < 2 || !video.videoWidth) return null; // 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 echelle = Math.min(1, largeurMax / video.videoWidth);
const l = Math.round(video.videoWidth * echelle); const l = Math.round(video.videoWidth * echelle);
const h = Math.round(video.videoHeight * echelle); const h = Math.round(video.videoHeight * echelle);
if (canvas.width !== l || canvas.height !== h) { canvas.width = l; canvas.height = h; } if (canvas.width !== l || canvas.height !== h) { canvas.width = l; canvas.height = h; }
ctx.drawImage(video, 0, 0, l, 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() { const symboles = await window.zbar.scanImageData(ctx.getImageData(0, 0, l, h), scanner);
return derniersPixels; 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, // Libérer la caméra : sinon le voyant reste allumé et la batterie se vide,
@@ -78,5 +122,6 @@ export function pixels() {
export function arreter() { export function arreter() {
if (flux) { flux.getTracks().forEach(t => t.stop()); flux = null; } if (flux) { flux.getTracks().forEach(t => t.stop()); flux = null; }
if (video) { video.srcObject = null; video = null; } if (video) { video.srcObject = null; video = null; }
canvas = null; ctx = null; derniersPixels = null; if (scanner) { scanner.destroy(); scanner = null; }
canvas = null; ctx = null;
} }
-102
View File
@@ -1,102 +0,0 @@
using MaBibli.Client.Scan;
using ZXing.Common;
using ZXing.OneD;
namespace MaBibli.Tests;
/// <summary>
/// Tests du décodage pur (aucune caméra, aucun navigateur) : on encode un EAN-13 avec
/// ZXing, on le convertit en buffer RGBA comme le ferait <c>getImageData</c>, et on
/// vérifie le trajet aller-retour. Aucune image externe n'est nécessaire.
/// </summary>
public class IsbnScannerTests
{
// Les ISBN de vérification du projet (voir CLAUDE.md).
[Theory]
[InlineData("9782070612758")] // Le Petit Prince
[InlineData("9782253004226")] // Germinal
[InlineData("9782080704092")] // Le Horla
[InlineData("9780262033848")] // Introduction to Algorithms
public void Decode_un_EAN13_genere(string isbn)
{
var (rgba, largeur, hauteur) = ImageEan13(isbn);
Assert.Equal(isbn, IsbnScanner.TryDecode(rgba, largeur, hauteur));
}
[Fact]
public void Decode_aussi_un_EAN8()
{
// Pas un ISBN, mais le format est dans les hints : le décodeur doit le lire,
// c'est la couche appelante qui écarte les codes non-ISBN.
var matrice = new EAN8Writer().encode("96385074", ZXing.BarcodeFormat.EAN_8, 260, 160);
var (rgba, largeur, hauteur) = VersRgba(matrice);
Assert.Equal("96385074", IsbnScanner.TryDecode(rgba, largeur, hauteur));
}
[Fact]
public void Renvoie_null_sur_une_frame_sans_code_barres()
{
// Cas très majoritaire en usage réel : la caméra filme autre chose.
// ZXing doit rendre null, jamais lever d'exception.
const int largeur = 640, hauteur = 480;
var rgba = new byte[largeur * hauteur * 4];
var alea = new Random(1234);
alea.NextBytes(rgba);
Assert.Null(IsbnScanner.TryDecode(rgba, largeur, hauteur));
}
[Fact]
public void Renvoie_null_sur_une_image_uniforme()
{
var rgba = new byte[320 * 200 * 4];
Array.Fill(rgba, (byte)255);
Assert.Null(IsbnScanner.TryDecode(rgba, 320, 200));
}
[Theory]
[InlineData(0, 100)]
[InlineData(100, 0)]
[InlineData(-5, 100)]
public void Renvoie_null_sur_des_dimensions_absurdes(int largeur, int hauteur)
=> Assert.Null(IsbnScanner.TryDecode(new byte[4000], largeur, hauteur));
[Fact]
public void Renvoie_null_si_le_buffer_est_trop_court()
{
// Une frame tronquée ne doit pas faire tomber la boucle de scan.
Assert.Null(IsbnScanner.TryDecode(new byte[100], 640, 480));
}
private static (byte[] Rgba, int Largeur, int Hauteur) ImageEan13(string isbn)
=> VersRgba(new EAN13Writer().encode(isbn, ZXing.BarcodeFormat.EAN_13, 380, 160));
/// <summary>
/// Convertit une BitMatrix en buffer RGBA, exactement la forme que le canvas
/// du navigateur fournit à C# (4 octets par pixel, opaque).
/// </summary>
private static (byte[] Rgba, int Largeur, int Hauteur) VersRgba(BitMatrix matrice)
{
var largeur = matrice.Width;
var hauteur = matrice.Height;
var rgba = new byte[largeur * hauteur * 4];
for (var y = 0; y < hauteur; y++)
{
for (var x = 0; x < largeur; x++)
{
var valeur = matrice[x, y] ? (byte)0 : (byte)255;
var i = ((y * largeur) + x) * 4;
rgba[i] = valeur;
rgba[i + 1] = valeur;
rgba[i + 2] = valeur;
rgba[i + 3] = 255;
}
}
return (rgba, largeur, hauteur);
}
}