Skip to content

Instantly share code, notes, and snippets.

@brahimmachkouri
Created August 30, 2025 16:09
Show Gist options
  • Save brahimmachkouri/505edc1eec2785ff0f1f4c862b04b871 to your computer and use it in GitHub Desktop.
Save brahimmachkouri/505edc1eec2785ff0f1f4c862b04b871 to your computer and use it in GitHub Desktop.
Conversion PDF -> CBZ/CBR
#!/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