diff --git a/CLAUDE.md b/CLAUDE.md index c82af3f..899773b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ``/`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.POSSIBLE_FORMATS] = new List - { - 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 +`