Skip to main content

25_StirlingPDF

RCH-20260530-0001 — Mise en place API StirlingPDF pour compression PDF automatisée (workflow geo_loud)

Date2026-05-30 ServiceStirlingPDF ContexteÉtape 3 du workflow geo_loud : automatiser la compression des 127 PDFs lourds (>75 Mo) déplacés dans \\NASMAISON\foxy\geo_loud\. Objectif : appeler l'API StirlingPDF depuis le PC, écraser chaque fichier avec sa version compressée, mettre à jour la DB SQLite. Fichier de test : 000209_L'inde.pdf (314 Mo). Outils utilisésStirlingPDF API REST (JWT Bearer) | curl.exe | plink (PuTTY) pour SSH nginx | Python 3.14 + requests | SQLite nas_geographie.db Requête APIPOST https://spdf.juxjux.ovh/api/v1/misc/compress-pdf — multipart/form-data champ fileInput, header Authorization: Bearer <token>

Résultats

Auth JWT résolue. nginx corrigé (client_max_body_size 500M). Test de compression réussi sur 000209_L'inde.pdf : 314 Mo → 311.7 Mo (0.7% de gain — PDF déjà optimisé en images JPEG). Script nas_geo_compress.py opérationnel pour traitement en batch de tous les fichiers.

Points clés

    Auth imposée : malgré DOCKER_ENABLE_SECURITY=false dans la stack Docker, StirlingPDF impose un compte admin au premier lancement. Compte créé : admin / Motdepasse18! Endpoint de login : POST /api/v1/auth/login avec body JSON {"username":"admin","password":"Motdepasse18!"} → réponse : JWT dans .session.access_token, valable 24h Utilisation du token : header Authorization: Bearer <token> sur chaque appel API nginx : client_max_body_size par défaut = 1 Mo → erreur 413 sur gros fichiers. Correction : ajout de client_max_body_size 500M; dans /etc/nginx/sites-enabled/spdf.juxjux.ovh (via plink/SSH), puis sudo nginx -s reload Endpoint compression : POST /api/v1/misc/compress-pdf — multipart avec champ fileInput — retourne le PDF compressé directement dans le corps de la réponse (binary) Gain variable : PDFs avec images JPEG déjà compressées → gain faible (<1%). PDFs texte/vectoriel ou images PNG → gain potentiellement important (30–70%) PowerShell inadapté aux gros fichiers : concaténation de byte arrays → OutOfMemoryException sur 314 Mo. Utiliser curl.exe ou Python requests Spec API : /v3/api-docs retourne le HTML de la SPA React. Documentation disponible dans la Swagger UI interactive : https://spdf.juxjux.ovh/swagger-ui/index.html (après login)

    Limites

      Token JWT expire après 24h — pour des batchs très longs, prévoir un re-login automatique La compression StirlingPDF n'expose pas de paramètre de niveau dans cette version — niveau par défaut uniquement Le fichier transite en RAM sur le VPS pendant le traitement — fichiers >500 Mo potentiellement problématiques selon la RAM disponible

      Script nas_geo_compress.py

      Emplacement : C:\Users\eliob\.claude\nas_geo_compress.py
      Lancement : py C:\Users\eliob\.claude\nas_geo_compress.py
      Dépendances : pip install requests (sqlite3 et os natifs Python)

      Le script ajoute automatiquement les colonnes taille_compresse_octets et date_compression à la table fichiers si elles n'existent pas encore.

      """
      Étape 3 du workflow geo_loud : compression PDF via StirlingPDF.
      - Lit les fichiers statut='deplace' dans nas_geographie.db
      - Envoie chaque PDF à l'API StirlingPDF
      - Écrase le fichier d'origine avec la version compressée
      - Met à jour la DB (taille_compresse_octets, date_compression)
      """
      import sqlite3
      import requests
      import os
      from datetime import datetime
      
      DB_PATH = r"C:\Users\eliob\Documents\nas_geographie.db"
      GEO_LOUD = r"\\NASMAISON\foxy\geo_loud"
      STIRLING_URL = "https://spdf.juxjux.ovh"
      STIRLING_USER = "admin"
      STIRLING_PASS = "Motdepasse18!"
      
      
      def get_token():
          r = requests.post(
              f"{STIRLING_URL}/api/v1/auth/login",
              json={"username": STIRLING_USER, "password": STIRLING_PASS},
              timeout=30,
          )
          r.raise_for_status()
          return r.json()["session"]["access_token"]
      
      
      def init_db_columns(conn):
          cur = conn.cursor()
          cols = {row[1] for row in cur.execute("PRAGMA table_info(fichiers)")}
          if "taille_compresse_octets" not in cols:
              cur.execute("ALTER TABLE fichiers ADD COLUMN taille_compresse_octets INTEGER")
          if "date_compression" not in cols:
              cur.execute("ALTER TABLE fichiers ADD COLUMN date_compression TEXT")
          conn.commit()
      
      
      def get_pending(conn):
          cur = conn.cursor()
          cur.execute("""
              SELECT id, nom, chemin_geo_loud, taille_octets
              FROM fichiers
              WHERE statut = 'deplace'
                AND extension IN ('.pdf', '.PDF')
                AND (date_compression IS NULL)
              ORDER BY taille_octets DESC
          """)
          return cur.fetchall()
      
      
      def compress_file(token, filepath):
          with open(filepath, "rb") as f:
              r = requests.post(
                  f"{STIRLING_URL}/api/v1/misc/compress-pdf",
                  headers={"Authorization": f"Bearer {token}"},
                  files={"fileInput": (os.path.basename(filepath), f, "application/pdf")},
                  timeout=600,
                  stream=True,
              )
          r.raise_for_status()
          return r.content
      
      
      def update_db(conn, file_id, taille_compresse):
          conn.execute("""
              UPDATE fichiers
              SET taille_compresse_octets = ?, date_compression = ?
              WHERE id = ?
          """, (taille_compresse, datetime.now().isoformat(), file_id))
          conn.commit()
      
      
      def fmt_mo(octets):
          return f"{octets / 1_048_576:.1f} Mo"
      
      
      def main():
          print("Connexion à StirlingPDF...")
          token = get_token()
          print("Token JWT OK")
      
          conn = sqlite3.connect(DB_PATH)
          init_db_columns(conn)
          pending = get_pending(conn)
          print(f"{len(pending)} fichier(s) PDF à compresser\n")
      
          if not pending:
              print("Rien à faire.")
              conn.close()
              return
      
          total_avant = sum(r[3] for r in pending)
          total_apres = 0
          ok = 0
          errors = []
      
          for i, (file_id, nom, chemin_geo_loud, taille_avant) in enumerate(pending, 1):
              filepath = chemin_geo_loud or os.path.join(GEO_LOUD, nom)
              if not os.path.exists(filepath):
                  print(f"[{i}/{len(pending)}] ABSENT  {nom}")
                  errors.append((nom, "fichier absent"))
                  continue
      
              taille_reel_avant = os.path.getsize(filepath)
              print(f"[{i}/{len(pending)}] {nom}")
              print(f"    avant : {fmt_mo(taille_reel_avant)}", end=" ", flush=True)
      
              try:
                  compressed = compress_file(token, filepath)
                  taille_apres_compression = len(compressed)
                  gain = (taille_reel_avant - taille_apres_compression) / taille_reel_avant * 100
      
                  tmp = filepath + ".tmp"
                  with open(tmp, "wb") as f:
                      f.write(compressed)
                  os.replace(tmp, filepath)
      
                  update_db(conn, file_id, taille_apres_compression)
                  total_apres += taille_apres_compression
                  ok += 1
                  print(f"-> après : {fmt_mo(taille_apres_compression)} (gain {gain:.1f}%)")
      
              except Exception as e:
                  print(f"-> ERREUR : {e}")
                  errors.append((nom, str(e)))
                  tmp = filepath + ".tmp"
                  if os.path.exists(tmp):
                      os.remove(tmp)
      
          conn.close()
      
          print(f"\n{'='*50}")
          print(f"Terminé : {ok}/{len(pending)} compressés")
          if ok:
              gain_total = (total_avant - total_apres) / total_avant * 100
              print(f"Volume avant : {fmt_mo(total_avant)}")
              print(f"Volume après : {fmt_mo(total_apres)}")
              print(f"Gain total   : {gain_total:.1f}%  ({fmt_mo(total_avant - total_apres)} récupérés)")
          if errors:
              print(f"\nErreurs ({len(errors)}) :")
              for nom, msg in errors:
                  print(f"  {nom} : {msg}")
      
      
      if __name__ == "__main__":
          main()
      

      Localisation des ressources

      URLshttps://spdf.juxjux.ovh (UI) | https://spdf.juxjux.ovh/swagger-ui/index.html (API docs, après login) CheminsScript : C:\Users\eliob\.claude\nas_geo_compress.py | DB : C:\Users\eliob\Documents\nas_geographie.db | Fichiers source : \\NASMAISON\foxy\geo_loud\ | nginx vhost : /etc/nginx/sites-enabled/spdf.juxjux.ovh IDsStack Portainer : stirlingpdf (ID 89) | Container : stirling-pdf | Port interne : 8090 Métadonnées{"fichiers_a_traiter": 127, "volume_total_go": 13.577, "test_fichier": "000209_L-inde.pdf", "test_avant_mo": 314, "test_apres_mo": 311.7, "test_gain_pct": 0.7, "nginx_max_body": "500M", "token_duree_h": 24}

      Tags

      #StirlingPDF #compression #PDF #geo_loud #API #JWT #nginx #Python #NAS #workflow

      Suite / Pistes

      1/ Lancer le script sur l'ensemble des 127 fichiers — surveiller le gain réel par catégorie de PDF.
      2/ Mettre à jour la page BookStack 210 avec les colonnes DB ajoutées (taille_compresse_octets, date_compression) et le bilan de compression une fois le batch terminé.
      3/ Si le gain moyen est insuffisant, explorer d'autres niveaux de compression StirlingPDF ou un outil alternatif (Ghostscript direct).
      4/ Après compression de tous les fichiers : lancer l'étape 4 (restauration) via nas_geo_loud.py restaurer.