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 |
| 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 |
| 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 » |
## 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.
- **Un seul langage**, cohérent avec le reste de la stack.
- 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.
**Constat d'usage** : 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**.
### 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 |
|---|---|
| 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) |
Ne jamais reconduire ce raccourci : pour juger un décodeur, il faut mesurer **ce qu'il lit**,
pas ce qu'il coûte.
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
été **vérifiée dans un vrai navigateur** (Chromium 148, x86_64 de bureau), sur le publish
`Release` du client, en appelant le décodeur depuis la console via `BancEssaiScan` :
**Non établi** : que le moteur soit *seul* en cause. L'ancienne implémentation ne faisait pas que
décoder, elle **recadrait** sur la bande centrale (45 % de la hauteur) après réduction à 640 px,
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 |
| Build `Debug`, interprété | 17,7 ms/frame | 28,7 ms/frame |
| Décodage | C#, `IsbnScanner.TryDecode` | JS, `window.zbar.scanImageData` |
| 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
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.
`IsbnScanner.cs`, `BancEssaiScan.cs` et `IsbnScannerTests.cs` ont disparu.
⚠️ Ne jamais juger la fluidité sur un build `Debug` : il est **4 à 5× plus lent** que le
`Release`, de quoi conclure à tort qu'il faut l'AOT.
⚠️ **Coût assumé : six tests de décodage en moins.** Ils s'exécutaient en C# sur des images
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`) :
**+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 :
Son `camera.js` ouvre la caméra lui-même et **avale les erreurs** :
| Assembly | Delta brotli |
|---|---|
| `zxing.wasm` | +192 495 o |
| `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;
}
}
```js
navigator.mediaDevices.getUserMedia(constraints).then(...).catch(function (error) {
console.log(error); // et c'est tout
});
```
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)
### La douchette USB est le vrai remède au scan raté sur PC