Created
August 30, 2025 16:09
-
-
Save brahimmachkouri/505edc1eec2785ff0f1f4c862b04b871 to your computer and use it in GitHub Desktop.
Conversion PDF -> CBZ/CBR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| pdf_to_cbx.py — Convertir un PDF en CBZ (ou CBR si 'rar' est disponible) | |
| ======================================================================= | |
| Dépendances Python : PyMuPDF (pymupdf), Pillow | |
| Installation rapide : | |
| pip install --upgrade pymupdf pillow | |
| Utilisation : | |
| python3 pdf_to_cbx.py "monfichier.pdf" | |
| # Options utiles : | |
| python3 pdf_to_cbx.py "monfichier.pdf" --dpi 300 --fmt jpg --qual 90 --cbr --out "MonComic" | |
| Notes : | |
| - Par défaut, crée un .cbz (ZIP). Si --cbr est donné et que l'outil 'rar' est présent, crée un .cbr. | |
| - Les images sont nommées page-001.jpg/png pour garantir l'ordre. | |
| """ | |
| import argparse | |
| import os | |
| import shutil | |
| import sys | |
| import tempfile | |
| import zipfile | |
| from pathlib import Path | |
| # PyMuPDF | |
| try: | |
| import fitz # type: ignore # pip install pymupdf | |
| except ImportError as e: | |
| sys.exit("❌ PyMuPDF n'est pas installé. Fais : pip install --upgrade pymupdf") | |
| # Pillow | |
| try: | |
| from PIL import Image | |
| except ImportError: | |
| sys.exit("❌ Pillow n'est pas installé. Fais : pip install --upgrade pillow") | |
| def render_pages_to_images(pdf_path: Path, out_dir: Path, dpi: int, fmt: str, quality: int) -> list[Path]: | |
| """ | |
| Rendre les pages du PDF en images. | |
| - dpi : résolution de rendu (300 conseillé) | |
| - fmt : 'jpg' ou 'png' | |
| - quality : qualité JPEG (si fmt=jpg), ignorée pour png | |
| """ | |
| fmt = fmt.lower() | |
| if fmt not in {"jpg", "jpeg", "png"}: | |
| raise ValueError("Format image non supporté. Utiliser 'jpg' ou 'png'.") | |
| print(f"→ Ouverture PDF : {pdf_path}") | |
| try: | |
| doc = fitz.open(pdf_path) | |
| except Exception as e: | |
| raise RuntimeError(f"Impossible d'ouvrir le PDF : {e}") | |
| if doc.needs_pass: | |
| raise RuntimeError("Le PDF est chiffré / protégé par mot de passe. Déchiffre-le d'abord (qpdf --decrypt).") | |
| scale = dpi / 72.0 | |
| mat = fitz.Matrix(scale, scale) | |
| out_paths: list[Path] = [] | |
| total = doc.page_count | |
| for i, page in enumerate(doc, start=1): | |
| try: | |
| pix = page.get_pixmap(matrix=mat, alpha=False) | |
| except Exception as e: | |
| raise RuntimeError(f"Echec rendu page {i}/{total}: {e}") | |
| fname = out_dir / f"page-{i:03d}.{ 'jpg' if fmt=='jpeg' else fmt}" | |
| if fmt in {"jpg", "jpeg"}: | |
| # Passage par Pillow pour contrôler la qualité JPEG | |
| mode = "RGB" | |
| img = Image.frombytes(mode, (pix.width, pix.height), pix.samples) | |
| img.save(fname, "JPEG", quality=quality, optimize=True, progressive=True) | |
| else: # png | |
| pix.save(str(fname)) | |
| print(f" ✓ Page {i}/{total} -> {fname.name}") | |
| out_paths.append(fname) | |
| doc.close() | |
| return out_paths | |
| def make_cbz(image_paths: list[Path], out_file: Path) -> None: | |
| """Créer un CBZ (ZIP) depuis la liste d'images.""" | |
| print(f"→ Création CBZ : {out_file.name}") | |
| with zipfile.ZipFile(out_file, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: | |
| for p in image_paths: | |
| zf.write(p, arcname=p.name) | |
| print(" ✓ CBZ créé") | |
| def make_cbr_with_rar(image_paths: list[Path], out_file: Path) -> bool: | |
| """Créer un CBR via l'outil 'rar' si disponible. Retourne True si réussi.""" | |
| rar = shutil.which("rar") or shutil.which("winrar") or shutil.which("rar.exe") | |
| if not rar: | |
| return False | |
| print(f"→ Création CBR (RAR trouvé : {rar}) : {out_file.name}") | |
| # Pour éviter les soucis d'espaces, on crée l'archive depuis le dossier images | |
| work_dir = image_paths[0].parent | |
| names = [p.name for p in image_paths] | |
| try: | |
| import subprocess | |
| cmd = [rar, "a", "-ep1", str(out_file)] + names | |
| subprocess.run(cmd, cwd=str(work_dir), check=True) | |
| print(" ✓ CBR créé") | |
| return True | |
| except Exception as e: | |
| print(f" ⚠️ Echec création CBR : {e}") | |
| return False | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description="PDF → CBZ/CBR (via PyMuPDF)") | |
| parser.add_argument("pdf", type=str, help="Chemin du fichier PDF source") | |
| parser.add_argument("--dpi", type=int, default=300, help="Résolution de rendu (par défaut 300)") | |
| parser.add_argument("--fmt", choices=["jpg", "png"], default="jpg", help="Format image de sortie (jpg|png)") | |
| parser.add_argument("--qual", type=int, default=90, help="Qualité JPEG si --fmt=jpg (1..95)") | |
| parser.add_argument("--out", type=str, default=None, help="Nom de base de sortie (sans extension)") | |
| parser.add_argument("--cbr", action="store_true", help="Tenter de produire un .cbr si 'rar' est disponible") | |
| args = parser.parse_args() | |
| pdf_path = Path(args.pdf).expanduser().resolve() | |
| if not pdf_path.exists(): | |
| print(f"❌ PDF introuvable : {pdf_path}") | |
| return 2 | |
| base = args.out or pdf_path.stem | |
| out_cbz = pdf_path.with_name(f"{base}.cbz") | |
| out_cbr = pdf_path.with_name(f"{base}.cbr") | |
| try: | |
| with tempfile.TemporaryDirectory(prefix="pdf2cbx_") as tmpdir: | |
| tmpdir = Path(tmpdir) | |
| images = render_pages_to_images(pdf_path, tmpdir, dpi=args.dpi, fmt=args.fmt, quality=args.qual) | |
| if args.cbr: | |
| if make_cbr_with_rar(images, out_cbr): | |
| # On crée aussi le CBZ pour compatibilité, sauf si tu ne veux vraiment que le CBR : commente la ligne suivante | |
| make_cbz(images, out_cbz) | |
| else: | |
| print(" ⚠️ 'rar' non disponible. Création d'un CBZ à la place.") | |
| make_cbz(images, out_cbz) | |
| else: | |
| make_cbz(images, out_cbz) | |
| except RuntimeError as e: | |
| print(f"❌ Erreur : {e}") | |
| return 1 | |
| except Exception as e: | |
| print(f"❌ Erreur inattendue : {e}") | |
| return 1 | |
| print("🎉 Terminé.") | |
| if out_cbr.exists(): | |
| print(f" → {out_cbr}") | |
| print(f" → {out_cbz}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment