12 KiB
12 KiB
#74 — Support complet des documents PDF
Statut : ✅ Terminé (2026-09-07 — C3 + Range 206 + config G3 + indexation incrémentale, commit
7042307. Optionnels D2/E4/H2/I2 non retenus) Effort : 4-5 jours | Impact : 🟡 Références : Roadmap · Changelog — 2.1.0
-
Description : Prise en charge native des fichiers PDF dans ObsiGate avec parité fonctionnelle complète avec les documents Markdown : apparition dans l'arborescence, indexation full-text, visualisation inline dans le navigateur, recherche TF-IDF, et téléchargement.
-
Implémentation réelle (vérifiée 2026-09-07) :
- Bugs corrigés (2026-09) :
api_pdf_streamcrashait en 500 (NameError: current_userjamais injecté) ; l'indexation incrémentale du watcher faisaitread_text()sur les PDFs (garbage) ; Range/206 etpdf/infoabsents malgré le texte ci-dessous. - BUG-060 (2026-09-17) : l'affichage inline ne fonctionnait plus — la CSP durcie en BUG-034 (
object-src 'none') bloquait l'<embed>du viewer (barre d'outils rendue, corps vide). Le rendu passe par une<iframe>(autorisée parframe-src 'self'), conforme à E1. Tests :tests/frontend/pdf-viewer.test.mjs+tests/e2e/pdf-viewer.spec.js. GET /api/file/{vault}/pdf/info— métadonnées seules sans transférer le document (C3)- Stream avec
Accept-Ranges+ 206 Partial Content (single range, suffix-range, 416) (C2) OBSIGATE_PDF_MAX_SIZE_MB(50) +OBSIGATE_PDF_EXTRACT_TIMEOUT(30s via thread-pool) (B4/G3)- Backend
backend/pdf_reader.py(existant) — extraction pypdf + pymupdf (fallback), métadonnées, TOC backend/indexer.py—.pdfdans SUPPORTED_EXTENSIONS, extraction dansindex_document()backend/main.py— flagis_pdf: Trueretourné parapi_file_view, endpointGET /api/file/{vault}/pdf/streamavec support Range/206backend/search.py— filtreext:pdf(déjà implémenté avant cette PR)frontend/js/viewer.js:451-480— brancheif (data.is_pdf)+ iframe + toolbar + TOC + bouton download- Tests :
tests/test_pdf.py(26 tests verts) — text/metadata/TOC + indexation scan/incrémentale + filtre ext + stream 200/206/416 + /pdf/info + limite de taille - Bug fixé dans cette PR :
PdfReaderNameError danspdf_reader.pyquand pymupdf est installé (la variablePdfReadern'était déclarée que dans la brancheexcept ImportError) backend/requirements-test.txt(nouveau) —reportlabpour générer des PDFs de test
- Bugs corrigés (2026-09) :
-
Sous-tâches :
A. Backend — Extraction de texte PDF (1-1.5 jour)
- A1. Dépendance :
pypdf>=4.0retenu dans requirements (pure Python, simplicité Docker) ; PyMuPDF (fitz) utilisé automatiquement en priorité s'il est importable — l'inverse du plan initial, fonctionnellement équivalent. - A2. Module
backend/pdf_reader.py: Créer un module dédié avec les fonctions :extract_pdf_text(file_path: Path) -> str: extrait tout le texte du PDF, page par page, avec séparateur\fentre pages. Gère les PDF encodés, protégés par mot de passe (retourne erreur explicite), et corrompus.extract_pdf_metadata(file_path: Path) -> dict: extrait titre, auteur, sujet, nombre de pages, taille.extract_pdf_preview(file_path: Path, max_chars: int = 100000) -> str: extrait les N premiers caractères pour l'indexation (limité parSEARCH_CONTENT_LIMIT).
- A3. Fallback pypdf : Si pymupdf non disponible (exception d'import), fallback automatique sur
pypdfavec un log warning. Code structuré avec une interface abstraite (PdfReaderprotocol) pour swap transparent.
B. Backend — Indexation des PDF (1 jour)
- B1. Ajout à
SUPPORTED_EXTENSIONS: Ajouter.pdfau set dansbackend/indexer.py:56. Déclencher un rebuild complet de l'index (incrémental via le file watcher pour les nouveaux PDFs). - B2. Lecture PDF dans les DEUX chemins d'indexation (
_scan_vault+_index_single_file_sync, utilisé par le watcher) : détection.pdf→extract_pdf_text(). Fix 2026-09 : seul le scan complet gérait les PDFs, l'incrémental indexait du garbage. - B3. Métadonnées PDF (adapté) : titre PDF prioritaire sur le nom de fichier dans l'index ;
pages/authorexposés viaapi_file_view+/pdf/info(non stockés dans l'entrée d'index). - B4. Gestion d'erreur robuste : PDF corrompu → log warning + skip (ne pas bloquer l'indexation). PDF volumineux (>50 Mo) → log info + extraction tronquée à
SEARCH_CONTENT_LIMIT. Timeout d'extraction configurable (30s par défaut).
C. Backend — API endpoints PDF (0.5 jour)
- C1. Modification de
api_file_view()(backend/main.py:2270) : Avant la tentative deread_text(), détecter.pdfpar extension. Pour les PDF :- Extraire le texte avec
extract_pdf_text() - Extraire les métadonnées (pages, auteur)
- Retourner une réponse structurée :
is_pdf: true,page_count,pdf_metadata,html(aperçu texte formaté),raw_length - Le champ
htmlcontient un rendu texte simple (pas de markdown) : texte paginé ou première page formatée
- Extraire le texte avec
- C2. Nouvel endpoint
GET /api/file/{vault}/pdf/stream: Sert le fichier PDF brut avecContent-Type: application/pdfetContent-Disposition: inlinepour visualisation dans le navigateur. Supporte leRangeheader (HTTP 206 Partial Content) pour le streaming progressif des gros PDFs — essentiel pour la performance sur des documents volumineux. - C3. Nouvel endpoint
GET /api/file/{vault}/pdf/info: Retourne les métadonnées seules (pages, titre, auteur) sans le contenu — permet à l'UI d'afficher les infos avant de charger le PDF lourd. - C4. Endpoint download : Déjà fonctionnel (
/api/file/{vault}/download) — aucun changement nécessaire.
D. Frontend — Arborescence de fichiers (0.5 jour)
- D1. Icône et filtre : L'icône PDF (
file-textde Lucide) est déjà mappée dansEXT_ICONS(frontend/js/utils.js:129). Une fois.pdfdansSUPPORTED_EXTENSIONS, les PDFs apparaissent automatiquement dans l'arborescence via l'APIlist_directory. Aucun changement UI nécessaire. - D2. Distinction visuelle — ⚪ NON RETENU : Sous-titre léger sous le nom du fichier dans l'arborescence indiquant le nombre de pages (ex: « 12 pages ») pour différencier rapidement les PDF des MD. Donnée disponible via l'API
pdf/info. - D3. Drag & drop et upload : Le mécanisme d'upload existant (
POST /api/file/{vault}/upload) fonctionne déjà pour tout type de fichier. Vérifier que le MIME typeapplication/pdfest correctement détecté et que le watcher réindexe automatiquement.
E. Frontend — Viewer PDF (1 jour)
- E1. Rendu inline natif : Utiliser le visualiseur PDF intégré du navigateur via
<iframe>pointant sur/api/file/{vault}/pdf/stream?path=.... Approche optimale :- Zéro dépendance JS supplémentaire
- Rendu identique à Chrome/Firefox/Safari natif
- Support natif du zoom, recherche dans le document, navigation par pages, rotation
- L'iframe s'adapte en hauteur (
height: 100%du content-area)
- E2. Détection dans le viewer : Dans
frontend/js/viewer.js, fonctionrenderFileContent()— ajouter une branche après la détectiondata.unsupported:- Si
data.is_pdf === true→ render l'iframe PDF au lieu du viewer markdown - Si le navigateur ne supporte pas le rendu PDF inline → fallback sur l'UI « binaire » avec bouton download + bouton « Ouvrir dans un nouvel onglet »
- Si
- E3. Barre d'outils PDF : Dans la barre d'outils du viewer (celle qui a déjà les boutons Copier, Source, .md, PDF, Éditer, pop-out), pour les fichiers PDF :
- Remplacer « Copier » / « Source » / « Éditer » par des actions spécifiques PDF
- Bouton « Télécharger » (.pdf) — déjà existant, fonctionne
- Bouton « Plein écran » — ouvre le PDF dans un nouvel onglet en plein écran
- Badge « N pages » indiquant le nombre de pages
- Bouton « pop-out » — gardé, ouvre le viewer PDF dans une popup séparée
- E4. Thème — ⚪ NON RETENU : L'iframe PDF est en dehors du DOM applicatif donc pas affecté par le thème dark/light. Ajouter un message discret « Le PDF s'affiche avec le thème de votre navigateur » si
_currentTheme === 'dark'(les PDFs en fond blanc dans un thème sombre peuvent surprendre). - E5. Responsive : L'iframe s'adapte à la largeur du content-area. En mode mobile, hauteur ajustée à la viewport. La toolbar mobile existante fonctionne avec les actions PDF.
F. Frontend — Recherche (0.5 jour)
- F1. Résultats de recherche : Les PDFs apparaissent dans les résultats via le TF-IDF existant (le texte extrait est indexé). Ajouter un badge visuel « PDF » à côté du titre dans les résultats de recherche pour distinguer les PDFs des MD — utiliser l'icône
file-text. - F2. Snippets de recherche : Les extraits de contexte montrent le texte extrait du PDF avec surlignage des termes recherchés — fonctionnement identique aux MD via le mécanisme de snippet existant dans
search.py. - F3. Filtres de recherche avancés : Ajouter
ext:pdfcomme filtre pour limiter la recherche aux PDFs uniquement (complément aux filtrescreated:,modified:,size:déjà prévus #34).
G. Docker & Dépendances (0.5 jour)
- G1. requirements.txt :
pypdf>=4.0retenu (pymupdf optionnel, utilisé s'il est importable). - G2. Dockerfile : Vérifier que l'image
python:3.11-slimdispose des libs système nécessaires pour pymupdf. Si besoin, ajouterlibmupdf-devou utiliserpypdf(pure Python) pour éviter la complexité. Recommandation : pypdf pour la simplicité Docker, pymupdf en option pour la performance. - G3. Configuration :
OBSIGATE_PDF_MAX_SIZE_MB(50) +OBSIGATE_PDF_EXTRACT_TIMEOUT(30s) — documentés dans.env.exampleet README FR/EN.
H. Tests (1 jour)
- H1. Tests unitaires backend :
test_pdf_reader.py: extraction texte PDF simple, PDF vide, PDF avec uniquement des images (OCR non requis — retourne chaîne vide), PDF protégé par mot de passe, PDF corrompu, extraction métadonnées- Fixtures : créer un PDF de test minimal (2 pages, texte simple) via
reportlabdans les fixtures de test test_pdf_indexing.py: vérifier qu'un PDF dans un vault est correctement indexé, que le texte est recherchable, queindex_document()gère l'extension.pdftest_pdf_api.py: endpointviewretourneis_pdf: true, endpointpdf/streamretourneapplication/pdf, endpointpdf/inforetourne les métadonnées
- H2. Tests frontend — ⚪ NON RETENU :
- Test d'intégration : naviguer vers un fichier PDF → l'iframe est rendue
- Test : fichier PDF dans les résultats de recherche
- Test : téléchargement de PDF fonctionnel
- H3. CI : Ajouter la fixture PDF de test dans les artefacts de CI. Les tests PDF sont sautés si pymupdf/pypdf n'est pas disponible.
I. Documentation utilisateur (inclus dans l'effort)
- I1. Mettre à jour README.md : mentionner le support PDF dans les formats supportés
- I2. Ajouter une note dans la FAQ : « Comment visualiser un PDF dans ObsiGate ? » — ⚪ NON RETENU (README + guide couvrent déjà le support PDF)
- I3. Documenter les limitations : pas d'OCR (PDFs scannés non recherchables), pas d'annotation PDF, pas d'édition de PDF
J. Points d'attention / Risques
- Performance : Un PDF de 500 pages peut générer beaucoup de texte →
SEARCH_CONTENT_LIMIT(100 Ko) limite l'indexation au début du document. Pour les PDFs volumineux, envisager une extraction paginée avecSEARCH_CONTENT_LIMITréparti sur les N premières pages. - Sécurité : Les PDFs malveillants (injections JS, liens externes) ne sont pas exécutés dans l'iframe par défaut (sandbox du navigateur). Ajouter
sandbox="allow-same-origin"sur l'iframe pour renforcer. - Mémoire : pymupdf charge le PDF entier en mémoire. Pour les très gros PDFs (>200 Mo), utiliser le streaming ou
pypdfqui supporte la lecture paresseuse. - Compatibilité navigateurs : Le rendu PDF natif fonctionne sur Chrome, Firefox, Edge, Safari. Safari iOS a des limitations sur les iframes PDF. Prévoir le fallback « ouvrir dans un nouvel onglet » pour ces cas.
- PDFs dans les vaults Obsidian : Obsidian Desktop ne gère pas nativement les PDFs (affichage via iframe système). ObsiGate apporte une valeur ajoutée en offrant la visualisation + recherche.