1
0

pdf_files.py 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  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: # bibliothèque manquante sur le serveur : à corriger par l'administrateur
  30. logger.warning("PDF %s illisible faute de bibliothèque : %s", name, exc)
  31. if "cryptography" in str(exc): # chiffrement AES-256 ; brotli (compression rare) lève la même erreur
  32. raise PdfError("Ce PDF est protégé et je ne peux pas l'ouvrir pour le moment.")
  33. raise PdfError("Je ne peux pas lire ce PDF pour le moment.")
  34. except Exception as exc: # fichier abîmé ou pas un PDF : pypdf lève des erreurs très variées
  35. logger.info("PDF %s illisible : %s", name, exc)
  36. raise PdfError("Je n'arrive pas à lire ce PDF.")
  37. if pages == 0:
  38. raise PdfError("Ce PDF ne contient aucune page.")
  39. if pages > max_pages:
  40. raise PdfError(f"Ce PDF a {pages} pages : je n'en lis que {max_pages} au maximum. "
  41. "Envoie-moi seulement les pages utiles.")
  42. logger.info("PDF %s : %d page(s), %.1f Mo", name, pages, len(data) / 1e6)
  43. return ImageData(PDF_MEDIA_TYPE, data, name=name, pages=pages)