pdf_files.py 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. """Lecture des PDF joints aux messages : envoyés tels quels à Claude, qui en lit le texte et les pages."""
  2. from __future__ import annotations
  3. import io
  4. import logging
  5. from pypdf import PdfReader
  6. from pypdf.errors import DependencyError
  7. from .errors import UserFacingError
  8. from .media import PDF_MEDIA_TYPE, ImageData
  9. logger = logging.getLogger("chatbot.pdf")
  10. logging.getLogger("pypdf").setLevel(logging.ERROR) # avertissements sur les PDF mal formés, sans gravité
  11. PDF_EXTENSIONS = (".pdf",)
  12. # 26,7 Mo une fois encodé en base64 (+33 %) : bien en dessous de la limite de l'API (128 Mo par requête).
  13. # Plusieurs PDF arrivés pendant une réponse partent au besoin dans des requêtes séparées (voir
  14. # media.REQUEST_MAX_BASE64) ; seul, un PDF peut dépasser ce budget.
  15. PDF_MAX_BYTES = 20_000_000
  16. class PdfError(UserFacingError):
  17. """PDF illisible ou refusé, avec un message affichable sur Discord."""
  18. def prepare_pdf(name: str, data: bytes, max_pages: int) -> ImageData:
  19. """Vérifie un PDF avant l'envoi à Claude : lisible, sans mot de passe, max_pages pages au plus.
  20. Fonction bloquante (lecture du PDF) : à appeler via asyncio.to_thread.
  21. """
  22. try:
  23. reader = PdfReader(io.BytesIO(data))
  24. if reader.is_encrypted and not reader.decrypt(""):
  25. raise PdfError("Ce PDF est protégé par un mot de passe, je ne peux pas le lire.")
  26. pages = len(reader.pages)
  27. except PdfError:
  28. raise
  29. except DependencyError as exc: # chiffrement AES sans la bibliothèque cryptography : à corriger côté serveur
  30. logger.warning("PDF %s chiffré illisible faute de bibliothèque de déchiffrement : %s", name, exc)
  31. raise PdfError("Ce PDF est protégé et je ne peux pas l'ouvrir pour le moment.")
  32. except Exception as exc: # fichier abîmé ou pas un PDF : pypdf lève des erreurs très variées
  33. logger.info("PDF %s illisible : %s", name, exc)
  34. raise PdfError("Je n'arrive pas à lire ce PDF.")
  35. if pages == 0:
  36. raise PdfError("Ce PDF ne contient aucune page.")
  37. if pages > max_pages:
  38. raise PdfError(f"Ce PDF a {pages} pages : je n'en lis que {max_pages} au maximum. "
  39. "Envoie-moi seulement les pages utiles.")
  40. logger.info("PDF %s : %d page(s), %.1f Mo", name, pages, len(data) / 1e6)
  41. return ImageData(PDF_MEDIA_TYPE, data, name=name, pages=pages)