97 lines
4.8 KiB
Markdown
97 lines
4.8 KiB
Markdown
# #108 — Support complet des images (arborescence, visionneuse, indexation)
|
||
|
||
> **Version livrée :** 2.17.0 · **Statut :** ✅ · **Impact :** 🟡
|
||
> **Zone :** backend (`indexer`, `main`, `media_types`, `media_thumbs`) + frontend
|
||
> (`viewer.js`, `utils.js`, `style.css`).
|
||
|
||
## Contexte
|
||
|
||
Seul l'affichage *inline* dans un document markdown (`![[image.png]]`) fonctionnait.
|
||
L'image isolée était **invisible dans l'arborescence** (filtrée par
|
||
`SUPPORTED_EXTENSIONS`) et son **affichage standalone était cassé** : le `<img>`
|
||
généré par `api_file_view()` pointait vers `/api/file/{vault}/raw`, un endpoint qui
|
||
renvoie du **JSON** (`FileRawResponse`) et non des octets d'image.
|
||
|
||
## Ce qui a été livré
|
||
|
||
### A. Arborescence & indexation
|
||
|
||
- **`backend/media_types.py`** (nouveau) : source unique des extensions
|
||
`IMAGE_EXTENSIONS`, `AUDIO_EXTENSIONS`, `VIDEO_EXTENSIONS` (+ helpers
|
||
`is_image`/`is_audio`/`is_video`/`is_media`/`media_mime_type`). Socle réutilisé
|
||
par #109. `attachment_indexer.py` et `api_file_view()` ne dupliquent plus la
|
||
liste.
|
||
- Les extensions image sont intégrées à `SUPPORTED_EXTENSIONS`
|
||
(`indexer.py`) **avec une branche binaire** : `_scan_vault` et
|
||
`_index_single_file_sync` indexent **nom / taille / mtime** et ne lisent
|
||
**jamais** les octets (`content: ""`, `content_preview: ""`). Le TF-IDF reste
|
||
donc propre et aucune `UnicodeDecodeError` ne pollue les logs.
|
||
- Le reindex **watchdog** suit automatiquement (même filtre d'extensions).
|
||
- Le filtre `ext:png` / `ext:jpg` de la recherche avancée est opérationnel dès
|
||
lors que les images entrent dans l'index.
|
||
- `/api/dashboard` expose `image_count` (par vault) et `total_images` (global),
|
||
séparés du `file_count` général.
|
||
|
||
### B. Affichage standalone (correctif)
|
||
|
||
- `api_file_view()` génère désormais
|
||
`src="/api/image/{vault}?path=…"` (chemin URL-encodé) au lieu de `/raw`.
|
||
- `viewer.js` utilise le même endpoint (plus de bouton « Plein écran » cassé).
|
||
- **Sécurité SVG** : `/api/image` (et le repli de `/api/media/.../thumb`) ajoute
|
||
`Content-Security-Policy: sandbox` pour les `.svg`, ce qui empêche
|
||
l'exécution du JavaScript embarqué quand le fichier est ouvert directement
|
||
dans un onglet (XSS same-origin). Dans une balise `<img>`, l'en-tête est sans
|
||
effet. Le middleware de sécurité ne remplace plus une politique stricte posée
|
||
par une route.
|
||
|
||
### C. Miniatures
|
||
|
||
- `GET /api/media/{vault}/thumb?path=…&size=…` : miniature **WebP** générée
|
||
avec `pillow>=10.0`, mise en cache sous
|
||
`<OBSIGATE_DATA_DIR>/.obsigate-cache/thumbs/{sha1}.webp`. La clé de cache
|
||
embarque **mtime + taille**, donc toute édition invalide naturellement la
|
||
vignette.
|
||
- Génération dans un thread (`run_in_executor`) avec **timeout 2 s** ; repli sur
|
||
l'original en cas d'échec. SVG : l'original est servi tel quel (Pillow ne
|
||
décode pas le SVG) ; GIF/WebP animés : première frame.
|
||
|
||
### D. Visionneuse
|
||
|
||
`renderImageViewer()` (`frontend/js/viewer.js`) remplace l'ancien rendu minimal :
|
||
|
||
- image centrée `object-fit: contain` ; **zoom molette 0,1×–8×**, **pan au
|
||
glisser** (Pointer Events), **double-clic = réinitialisation**, raccourcis
|
||
`+` / `-` / `0` ;
|
||
- boutons +/−/reset et **badge de zoom** ;
|
||
- **navigation ←/→** entre les images du même dossier (via `/api/browse`) et
|
||
**pellicule de miniatures** (`/api/media/.../thumb`, `loading="lazy"`) ;
|
||
- barre d'outils : « Ouvrir l'original » (nouvel onglet `/api/image`),
|
||
téléchargement, **panneau métadonnées** repliable (dimensions via
|
||
`naturalWidth/Height`, taille, type MIME, chemin, date), **lightbox** plein
|
||
écran (fond `rgba(0,0,0,.9)`, `Échap` pour quitter) ;
|
||
- `EXT_ICONS` : extensions image → icône Lucide `image` ;
|
||
- compatible Split View (#75) : rendu dans `getContentArea()` du panneau actif.
|
||
|
||
### E. Tests
|
||
|
||
- `tests/test_image_api.py` : octets + MIME sur `/api/image`, en-tête `sandbox`
|
||
des SVG, URL `/api/image` dans le HTML de `api_file_view`, encodage des
|
||
chemins accentués, miniatures WebP + repli SVG + refus non-image.
|
||
- `tests/test_image_indexing.py` : image présente dans `list_directory` et
|
||
`path_index`, indexée avec `content == ""`, pertinence watchdog, compteurs
|
||
dashboard, filtre `ext:png`.
|
||
- `tests/frontend/image-viewer.test.mjs` : helpers purs (`clampImageZoom`,
|
||
`isImagePath`, `buildImageUrl`) + vérifications statiques (zoom/pan/nav,
|
||
absence de `/raw` dans la visionneuse, CSS, icônes).
|
||
- `tests/e2e/image-viewer.spec.js` : ouverture d'une image depuis
|
||
l'arborescence, réponse `/api/image` en `image/png`, zoom molette, navigation
|
||
par la pellicule (fixtures `test_vault/sample-image.png` +
|
||
`sample-vector.svg`).
|
||
|
||
## Limitations connues
|
||
|
||
- **HEIC/HEIF** (iPhone) : non décodables par les navigateurs → hors scope ;
|
||
`pillow-heif` envisagé en v2.
|
||
- Le SVG passe par l'original (pas de rendu bitmap côté serveur) : les
|
||
miniatures de dossiers SVG ne sont pas générées.
|