# Fichier: python_cheats/cheatsheets/pypdf.txt
# Cheatsheet PyPDF - Guide Complet pour Manipulation de PDF


[OK] INSTALLATION

# Installer pypdf (nouvelle version, recommandée)
pip install pypdf

# Installer PyPDF2 (ancienne version, maintenance minimale)
pip install PyPDF2

# Avec extras pour fonctionnalités avancées
pip install pypdf[crypto]           # Support cryptographie avancée
pip install pypdf[image]            # Support manipulation images
pip install pypdf[full]             # Toutes les fonctionnalités

# Note: pypdf est le successeur de PyPDF2
# PyPDF2 -> pypdf (même API, améliorations)


[OK] IMPORTS

from pypdf import PdfReader, PdfWriter, PdfMerger
from pypdf import Transformation, PageObject
from pypdf.generic import RectangleObject
from pypdf.annotations import FreeText, Text, Link, Rectangle

# Imports PyPDF2 (compatibilité)
from PyPDF2 import PdfReader, PdfWriter, PdfMerger


[OK] LECTURE DE PDF


# === Ouvrir un PDF ===

# Méthode basique
reader = PdfReader("document.pdf")

# Avec gestion contexte (recommandé)
with open("document.pdf", "rb") as file:
    reader = PdfReader(file)
    # Traitement...

# Depuis bytes
with open("document.pdf", "rb") as file:
    pdf_bytes = file.read()
    reader = PdfReader(io.BytesIO(pdf_bytes))

# Depuis URL
import requests
import io

response = requests.get("https://example.com/document.pdf")
reader = PdfReader(io.BytesIO(response.content))


# === Informations du document ===

# Nombre de pages
num_pages = len(reader.pages)
print(f"Nombre de pages: {num_pages}")

# Métadonnées
metadata = reader.metadata
print(f"Titre: {metadata.title}")
print(f"Auteur: {metadata.author}")
print(f"Sujet: {metadata.subject}")
print(f"Créateur: {metadata.creator}")
print(f"Producteur: {metadata.producer}")
print(f"Date création: {metadata.creation_date}")
print(f"Date modification: {metadata.modification_date}")

# Toutes les métadonnées
for key, value in metadata.items():
    print(f"{key}: {value}")

# Vérifier si PDF est crypté
is_encrypted = reader.is_encrypted
print(f"Crypté: {is_encrypted}")

# Layout mode
layout = reader.page_layout
print(f"Layout: {layout}")

# Page mode
page_mode = reader.page_mode
print(f"Mode: {page_mode}")


# === Accéder aux pages ===

# Première page
first_page = reader.pages[0]

# Dernière page
last_page = reader.pages[-1]

# Page spécifique (index commence à 0)
page = reader.pages[5]

# Itérer sur toutes les pages
for page_num, page in enumerate(reader.pages):
    print(f"Page {page_num + 1}")

# Slice de pages
pages_1_to_5 = reader.pages[0:5]


# === Extraire le texte ===

# Texte d'une page
page = reader.pages[0]
text = page.extract_text()
print(text)

# Texte avec orientations
text_with_orientation = page.extract_text(orientations=(0, 90, 180, 270))

# Texte de toutes les pages
full_text = ""
for page in reader.pages:
    full_text += page.extract_text()

# Extraire texte avec layout préservé (expérimental)
layout_text = page.extract_text(extraction_mode="layout")

# Extraire texte ligne par ligne
def extract_text_by_line(page):
    """Extrait le texte ligne par ligne"""
    text = page.extract_text()
    return text.split('\n')

lines = extract_text_by_line(reader.pages[0])


# === Informations sur les pages ===

page = reader.pages[0]

# Dimensions de la page
width = page.mediabox.width
height = page.mediabox.height
print(f"Dimensions: {width} x {height} points")

# Rotation de la page
rotation = page.get("/Rotate", 0)
print(f"Rotation: {rotation}°")

# CropBox, BleedBox, TrimBox, ArtBox
cropbox = page.cropbox
bleedbox = page.bleedbox if hasattr(page, 'bleedbox') else None
trimbox = page.trimbox if hasattr(page, 'trimbox') else None
artbox = page.artbox if hasattr(page, 'artbox') else None

# Ressources de la page
resources = page.get_contents()


[OK] EXTRACTION D'IMAGES


# === Extraire toutes les images d'une page ===

from pypdf import PdfReader
import io
from PIL import Image

reader = PdfReader("document.pdf")
page = reader.pages[0]

# Méthode 1: Via images
if hasattr(page, 'images'):
    for image in page.images:
        # image.name: nom de l'image
        # image.data: données binaires
        pil_image = Image.open(io.BytesIO(image.data))
        pil_image.save(f"{image.name}.png")

# Méthode 2: Extraction manuelle
def extract_images_from_page(page, output_folder="extracted_images"):
    """Extrait toutes les images d'une page"""
    import os
    os.makedirs(output_folder, exist_ok=True)
    
    if "/XObject" in page["/Resources"]:
        xObject = page["/Resources"]["/XObject"].get_object()
        
        for obj_name in xObject:
            obj = xObject[obj_name]
            
            if obj["/Subtype"] == "/Image":
                # Dimensions
                width = obj["/Width"]
                height = obj["/Height"]
                
                # Données de l'image
                data = obj.get_data()
                
                # Type d'image
                if obj["/ColorSpace"] == "/DeviceRGB":
                    mode = "RGB"
                else:
                    mode = "P"
                
                # Créer image PIL
                if "/Filter" in obj:
                    if obj["/Filter"] == "/DCTDecode":
                        # JPEG
                        img_path = f"{output_folder}/{obj_name[1:]}.jpg"
                        with open(img_path, "wb") as img_file:
                            img_file.write(data)
                    elif obj["/Filter"] == "/FlateDecode":
                        # PNG ou autre
                        img = Image.frombytes(mode, (width, height), data)
                        img.save(f"{output_folder}/{obj_name[1:]}.png")

# Extraire images de toutes les pages
def extract_all_images(pdf_path, output_folder="images"):
    reader = PdfReader(pdf_path)
    for page_num, page in enumerate(reader.pages):
        page_folder = f"{output_folder}/page_{page_num + 1}"
        extract_images_from_page(page, page_folder)


[OK] ÉCRITURE ET CRÉATION DE PDF


# === Créer un nouveau PDF ===

from pypdf import PdfWriter

writer = PdfWriter()

# Ajouter page vierge
page = PageObject.create_blank_page(width=612, height=792)  # Letter size
writer.add_page(page)

# Ajouter page avec dimensions personnalisées
page_a4 = PageObject.create_blank_page(width=595, height=842)  # A4
writer.add_page(page_a4)

# Sauvegarder
with open("nouveau.pdf", "wb") as output_file:
    writer.write(output_file)


# === Copier pages d'un PDF existant ===

reader = PdfReader("source.pdf")
writer = PdfWriter()

# Copier toutes les pages
for page in reader.pages:
    writer.add_page(page)

# Copier pages spécifiques
writer.add_page(reader.pages[0])
writer.add_page(reader.pages[5])
writer.add_page(reader.pages[10])

# Sauvegarder
with open("copie.pdf", "wb") as output_file:
    writer.write(output_file)


# === Ajouter métadonnées ===

writer = PdfWriter()
# ... ajouter pages ...

writer.add_metadata({
    "/Title": "Mon Document",
    "/Author": "Jean Dupont",
    "/Subject": "Rapport annuel",
    "/Creator": "Mon Application",
    "/Producer": "pypdf",
    "/Keywords": "rapport, finance, 2024"
})

# Sauvegarder avec métadonnées
with open("avec_metadata.pdf", "wb") as output_file:
    writer.write(output_file)


# === Cloner métadonnées d'un PDF existant ===

reader = PdfReader("source.pdf")
writer = PdfWriter()

# Copier pages
for page in reader.pages:
    writer.add_page(page)

# Cloner métadonnées
if reader.metadata:
    writer.add_metadata(reader.metadata)

with open("clone.pdf", "wb") as output_file:
    writer.write(output_file)


[OK] FUSION DE PDF (MERGE)


# === PdfMerger - Méthode recommandée ===

from pypdf import PdfMerger

merger = PdfMerger()

# Ajouter PDF entiers
merger.append("doc1.pdf")
merger.append("doc2.pdf")
merger.append("doc3.pdf")

# Sauvegarder
merger.write("fusion.pdf")
merger.close()

# Avec context manager (recommandé)
with PdfMerger() as merger:
    merger.append("doc1.pdf")
    merger.append("doc2.pdf")
    merger.write("fusion.pdf")


# === Fusionner avec plages de pages ===

merger = PdfMerger()

# Ajouter pages spécifiques (pages 1-5 de doc1)
merger.append("doc1.pdf", pages=(0, 5))

# Ajouter pages avec slice Python
merger.append("doc2.pdf", pages=(5, 10))

# Ajouter à partir d'une page jusqu'à la fin
merger.append("doc3.pdf", pages=(10, None))

merger.write("fusion_partielle.pdf")
merger.close()


# === Insérer à une position spécifique ===

merger = PdfMerger()

merger.append("doc1.pdf")
merger.merge(position=1, fileobj="insert.pdf")  # Insérer à la position 1
merger.append("doc2.pdf")

merger.write("insertion.pdf")
merger.close()


# === Fusionner avec signets (bookmarks) ===

merger = PdfMerger()

# Ajouter avec signet
merger.append("doc1.pdf", bookmark="Section 1")
merger.append("doc2.pdf", bookmark="Section 2")
merger.append("doc3.pdf", bookmark="Section 3")

# Ajouter avec signet imbriqué
merger.append("doc4.pdf", bookmark="Annexes", import_outline=True)

merger.write("avec_signets.pdf")
merger.close()


# === Fusionner plusieurs PDF (fonction utilitaire) ===

def merge_pdfs(pdf_list, output_path):
    """Fusionne une liste de PDF"""
    merger = PdfMerger()
    
    for pdf in pdf_list:
        merger.append(pdf)
    
    merger.write(output_path)
    merger.close()

# Utilisation
pdf_files = ["doc1.pdf", "doc2.pdf", "doc3.pdf"]
merge_pdfs(pdf_files, "resultat.pdf")


# === Fusionner tous les PDF d'un dossier ===

import os
from pathlib import Path

def merge_folder_pdfs(folder_path, output_path):
    """Fusionne tous les PDF d'un dossier"""
    merger = PdfMerger()
    
    # Trier les fichiers par nom
    pdf_files = sorted(Path(folder_path).glob("*.pdf"))
    
    for pdf_file in pdf_files:
        print(f"Ajout de {pdf_file.name}")
        merger.append(str(pdf_file))
    
    merger.write(output_path)
    merger.close()
    print(f"Fusion terminée: {output_path}")

# Utilisation
merge_folder_pdfs("mes_pdfs", "fusion_complete.pdf")


[OK] DIVISION DE PDF (SPLIT)


# === Extraire une seule page ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

# Extraire page 5 (index 4)
writer.add_page(reader.pages[4])

with open("page_5.pdf", "wb") as output_file:
    writer.write(output_file)


# === Extraire plage de pages ===

def extract_pages(input_pdf, output_pdf, start_page, end_page):
    """Extrait une plage de pages (indices Python)"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page_num in range(start_page, end_page):
        writer.add_page(reader.pages[page_num])
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)

# Extraire pages 10 à 20
extract_pages("document.pdf", "pages_10_20.pdf", 9, 20)


# === Diviser en pages individuelles ===

def split_pdf_to_pages(input_pdf, output_folder="pages"):
    """Divise un PDF en pages individuelles"""
    import os
    os.makedirs(output_folder, exist_ok=True)
    
    reader = PdfReader(input_pdf)
    
    for page_num, page in enumerate(reader.pages):
        writer = PdfWriter()
        writer.add_page(page)
        
        output_path = f"{output_folder}/page_{page_num + 1}.pdf"
        with open(output_path, "wb") as output_file:
            writer.write(output_file)
        
        print(f"Créé: {output_path}")

# Utilisation
split_pdf_to_pages("document.pdf")


# === Diviser en chunks de N pages ===

def split_pdf_by_chunks(input_pdf, pages_per_chunk=10, output_folder="chunks"):
    """Divise un PDF en chunks de N pages"""
    import os
    os.makedirs(output_folder, exist_ok=True)
    
    reader = PdfReader(input_pdf)
    total_pages = len(reader.pages)
    
    chunk_num = 1
    for i in range(0, total_pages, pages_per_chunk):
        writer = PdfWriter()
        
        # Ajouter les pages du chunk
        for page_num in range(i, min(i + pages_per_chunk, total_pages)):
            writer.add_page(reader.pages[page_num])
        
        output_path = f"{output_folder}/chunk_{chunk_num}.pdf"
        with open(output_path, "wb") as output_file:
            writer.write(output_file)
        
        print(f"Créé: {output_path}")
        chunk_num += 1

# Diviser en chunks de 5 pages
split_pdf_by_chunks("document.pdf", pages_per_chunk=5)


# === Diviser en pages paires et impaires ===

def split_odd_even_pages(input_pdf):
    """Sépare pages paires et impaires"""
    reader = PdfReader(input_pdf)
    
    writer_odd = PdfWriter()
    writer_even = PdfWriter()
    
    for page_num, page in enumerate(reader.pages):
        if (page_num + 1) % 2 == 1:  # Impair
            writer_odd.add_page(page)
        else:  # Pair
            writer_even.add_page(page)
    
    with open("pages_impaires.pdf", "wb") as output_file:
        writer_odd.write(output_file)
    
    with open("pages_paires.pdf", "wb") as output_file:
        writer_even.write(output_file)

split_odd_even_pages("document.pdf")


[OK] ROTATION DE PAGES


# === Rotation simple ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

page = reader.pages[0]

# Rotation de 90° dans le sens horaire
page.rotate(90)
writer.add_page(page)

# Rotation de 180°
page2 = reader.pages[1]
page2.rotate(180)
writer.add_page(page2)

# Rotation de 270° (90° anti-horaire)
page3 = reader.pages[2]
page3.rotate(270)
writer.add_page(page3)

with open("rotated.pdf", "wb") as output_file:
    writer.write(output_file)


# === Rotation de toutes les pages ===

def rotate_all_pages(input_pdf, output_pdf, angle):
    """Rotation de toutes les pages"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        page.rotate(angle)
        writer.add_page(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)

rotate_all_pages("document.pdf", "rotated_90.pdf", 90)


# === Rotation conditionnelle (pages paysage) ===

def rotate_landscape_to_portrait(input_pdf, output_pdf):
    """Rotation des pages paysage en portrait"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        # Vérifier si paysage
        if page.mediabox.width > page.mediabox.height:
            page.rotate(90)
        writer.add_page(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


# === Rotation spécifique de pages ===

def rotate_specific_pages(input_pdf, output_pdf, page_rotations):
    """
    Rotation spécifique de pages
    page_rotations: dict {page_num: angle}
    """
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page_num, page in enumerate(reader.pages):
        if page_num in page_rotations:
            page.rotate(page_rotations[page_num])
        writer.add_page(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)

# Rotation pages 0, 2, 5
rotations = {0: 90, 2: 180, 5: 270}
rotate_specific_pages("document.pdf", "custom_rotation.pdf", rotations)


[OK] MISE À L'ÉCHELLE ET RECADRAGE


# === Mise à l'échelle (Scale) ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

page = reader.pages[0]

# Agrandir de 150%
page.scale_by(1.5)
writer.add_page(page)

# Réduire de 50%
page2 = reader.pages[1]
page2.scale_by(0.5)
writer.add_page(page2)

# Scale avec facteurs différents pour X et Y
page3 = reader.pages[2]
page3.scale_by(sx=1.5, sy=1.2)
writer.add_page(page3)

with open("scaled.pdf", "wb") as output_file:
    writer.write(output_file)


# === Scale to fit (ajuster à une taille) ===

def scale_to_fit(page, target_width, target_height):
    """Mise à l'échelle pour tenir dans dimensions cibles"""
    current_width = float(page.mediabox.width)
    current_height = float(page.mediabox.height)
    
    # Calculer ratios
    width_ratio = target_width / current_width
    height_ratio = target_height / current_height
    
    # Utiliser le plus petit ratio pour tenir dans la zone
    scale_factor = min(width_ratio, height_ratio)
    
    page.scale_by(scale_factor)
    return page

# Ajuster à A4 (595 x 842 points)
reader = PdfReader("document.pdf")
writer = PdfWriter()

for page in reader.pages:
    scaled_page = scale_to_fit(page, 595, 842)
    writer.add_page(scaled_page)

with open("scaled_to_a4.pdf", "wb") as output_file:
    writer.write(output_file)


# === Recadrage (Crop) ===

page = reader.pages[0]

# Recadrer avec coordonnées absolues
# cropbox(left, bottom, right, top)
page.cropbox.lower_left = (50, 50)
page.cropbox.upper_right = (550, 750)

# Ou méthode complète
from pypdf.generic import RectangleObject
page.cropbox = RectangleObject([50, 50, 550, 750])

writer.add_page(page)


# === Recadrage avec marges ===

def crop_margins(page, left=0, right=0, top=0, bottom=0):
    """
    Recadre les marges
    left, right, top, bottom: marges en points à retirer
    """
    current_box = page.mediabox
    
    new_left = float(current_box.left) + left
    new_bottom = float(current_box.bottom) + bottom
    new_right = float(current_box.right) - right
    new_top = float(current_box.top) - top
    
    page.cropbox = RectangleObject([new_left, new_bottom, new_right, new_top])
    return page

# Retirer 50 points de chaque côté
reader = PdfReader("document.pdf")
writer = PdfWriter()

for page in reader.pages:
    cropped_page = crop_margins(page, 50, 50, 50, 50)
    writer.add_page(cropped_page)

with open("cropped.pdf", "wb") as output_file:
    writer.write(output_file)


[OK] TRANSFORMATIONS AVANCÉES


from pypdf import Transformation

# === Translation (déplacement) ===

reader = PdfReader("document.pdf")
writer = PdfWriter()

page = reader.pages[0]

# Déplacer de 100 points à droite, 50 vers le haut
op = Transformation().translate(tx=100, ty=50)
page.add_transformation(op)

writer.add_page(page)


# === Rotation avec centre personnalisé ===

# Rotation de 45° autour du point (300, 400)
op = Transformation().rotate(45)
page.add_transformation(op)


# === Composition de transformations ===

# Translation puis rotation
op = Transformation().translate(tx=100, ty=50).rotate(45)
page.add_transformation(op)

# Scale puis translation
op = Transformation().scale(sx=1.5, sy=1.5).translate(tx=50, ty=50)
page.add_transformation(op)


# === Transformation matricielle personnalisée ===

# Matrice de transformation [a b c d e f]
# a, d: scale
# b, c: skew
# e, f: translation
op = Transformation([[1.5, 0, 0, 1.5, 100, 100]])
page.add_transformation(op)


[OK] FILIGRANE (WATERMARK)


# === Ajouter filigrane à toutes les pages ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

# Charger le filigrane
watermark_reader = PdfReader("watermark.pdf")
watermark_page = watermark_reader.pages[0]

# Appliquer à toutes les pages
for page in reader.pages:
    page.merge_page(watermark_page)
    writer.add_page(page)

with open("watermarked.pdf", "wb") as output_file:
    writer.write(output_file)


# === Créer filigrane texte ===

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
import io

def create_text_watermark(text, output_path=None):
    """Crée un PDF avec texte en filigrane"""
    packet = io.BytesIO()
    can = canvas.Canvas(packet, pagesize=letter)
    
    # Configuration texte
    can.setFont("Helvetica", 60)
    can.setFillColorRGB(0.5, 0.5, 0.5, alpha=0.3)  # Gris transparent
    
    # Rotation et position
    can.saveState()
    can.translate(300, 400)
    can.rotate(45)
    can.drawCentredString(0, 0, text)
    can.restoreState()
    
    can.save()
    
    packet.seek(0)
    
    if output_path:
        with open(output_path, "wb") as f:
            f.write(packet.read())
        packet.seek(0)
    
    return PdfReader(packet)

# Créer et appliquer filigrane
watermark = create_text_watermark("CONFIDENTIEL")
watermark_page = watermark.pages[0]

reader = PdfReader("document.pdf")
writer = PdfWriter()

for page in reader.pages:
    page.merge_page(watermark_page)
    writer.add_page(page)

with open("confidential.pdf", "wb") as output_file:
    writer.write(output_file)


# === Filigrane en arrière-plan (underlay) ===

def add_background(page, background_page):
    """Ajoute une page en arrière-plan"""
    # Créer nouvelle page avec fond
    new_page = background_page
    new_page.merge_page(page)
    return new_page

reader = PdfReader("document.pdf")
background_reader = PdfReader("background.pdf")
writer = PdfWriter()

background_page = background_reader.pages[0]

for page in reader.pages:
    new_page = add_background(page, background_page)
    writer.add_page(new_page)


# === Filigrane avec position personnalisée ===

def add_positioned_watermark(page, watermark_page, x=0, y=0):
    """Ajoute filigrane à position spécifique"""
    from pypdf import Transformation
    
    # Déplacer le filigrane
    watermark_page.add_transformation(Transformation().translate(tx=x, ty=y))
    page.merge_page(watermark_page)
    return page


[OK] CRYPTAGE ET SÉCURITÉ


# === Crypter un PDF ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

# Copier toutes les pages
for page in reader.pages:
    writer.add_page(page)

# Crypter avec mot de passe utilisateur
writer.encrypt(user_password="motdepasse123")

with open("encrypted.pdf", "wb") as output_file:
    writer.write(output_file)


# === Crypter avec mot de passe propriétaire ===

# Mot de passe utilisateur: pour ouvrir
# Mot de passe propriétaire: pour modifier permissions
writer.encrypt(
    user_password="user_pass",
    owner_password="owner_pass"
)


# === Crypter avec permissions spécifiques ===

writer.encrypt(
    user_password="user123",
    owner_password="owner123",
    permissions_flag=-1,  # Toutes permissions par défaut
)

# Permissions spécifiques (combinaison bitwise)
from pypdf.constants import UserAccessPermissions

writer.encrypt(
    user_password="user123",
    owner_password="owner123",
    permissions_flag=(
        UserAccessPermissions.PRINT |
        UserAccessPermissions.MODIFY_ANNOTATIONS
    )
)


# === Permissions disponibles ===

# UserAccessPermissions:
# - PRINT: Impression
# - MODIFY: Modification
# - EXTRACT: Extraction de contenu
# - MODIFY_ANNOTATIONS: Modifier annotations
# - FILL_FORMS: Remplir formulaires
# - EXTRACT_ACCESSIBILITY: Extraction pour accessibilité
# - ASSEMBLE: Assembler document
# - PRINT_HIGH_QUALITY: Impression haute qualité


# === Décrypter un PDF ===

reader = PdfReader("encrypted.pdf")

# Vérifier si crypté
if reader.is_encrypted:
    # Décrypter
    reader.decrypt("motdepasse123")
    
    # Maintenant on peut accéder au contenu
    text = reader.pages[0].extract_text()


# === Retirer le cryptage (créer version non cryptée) ===

def decrypt_pdf(input_pdf, output_pdf, password):
    """Crée une version non cryptée"""
    reader = PdfReader(input_pdf)
    
    if reader.is_encrypted:
        reader.decrypt(password)
    
    writer = PdfWriter()
    
    for page in reader.pages:
        writer.add_page(page)
    
    # Pas de cryptage
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)

decrypt_pdf("encrypted.pdf", "decrypted.pdf", "motdepasse123")


# === Vérifier permissions ===

reader = PdfReader("document.pdf")

if reader.is_encrypted:
    reader.decrypt("password")
    
    # Vérifier permissions spécifiques
    # Note: API peut varier selon version pypdf
    if hasattr(reader, 'user_access_permissions'):
        perms = reader.user_access_permissions
        can_print = perms & UserAccessPermissions.PRINT
        can_modify = perms & UserAccessPermissions.MODIFY


[OK] SIGNETS ET STRUCTURE (BOOKMARKS)


# === Lire les signets ===

from pypdf import PdfReader

reader = PdfReader("document.pdf")

# Obtenir la structure des signets (outline)
outlines = reader.outline

# Afficher tous les signets
def print_outline(outlines, indent=0):
    """Affiche récursivement la structure des signets"""
    for item in outlines:
        if isinstance(item, list):
            # Sous-niveau
            print_outline(item, indent + 2)
        else:
            # Signet
            title = item.get('/Title', 'Sans titre')
            print("  " * indent + f"- {title}")
            
            # Page de destination
            if '/Page' in item:
                page = item['/Page']
                print("  " * indent + f"  -> Page: {page}")

print_outline(outlines)


# === Parcourir les signets avec métadonnées ===

def extract_bookmarks_info(reader):
    """Extrait informations détaillées des signets"""
    bookmarks = []
    
    def process_outline(outline, level=0):
        for item in outline:
            if isinstance(item, list):
                process_outline(item, level + 1)
            else:
                bookmark_info = {
                    'title': item.get('/Title', ''),
                    'level': level,
                    'page': None
                }
                
                # Trouver numéro de page
                if '/Page' in item:
                    page_obj = item['/Page']
                    # Trouver l'index de la page
                    for i, page in enumerate(reader.pages):
                        if page.indirect_reference == page_obj:
                            bookmark_info['page'] = i + 1
                            break
                
                bookmarks.append(bookmark_info)
    
    if reader.outline:
        process_outline(reader.outline)
    
    return bookmarks

# Utilisation
bookmarks = extract_bookmarks_info(reader)
for bm in bookmarks:
    indent = "  " * bm['level']
    print(f"{indent}{bm['title']} (page {bm['page']})")


# === Ajouter des signets ===

from pypdf import PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

# Copier pages
for page in reader.pages:
    writer.add_page(page)

# Ajouter signet simple (page 1)
writer.add_outline_item("Chapitre 1", 0)

# Ajouter signet (page 10)
writer.add_outline_item("Chapitre 2", 9)

# Ajouter signet avec parent (hiérarchie)
parent = writer.add_outline_item("Partie 1", 0)
writer.add_outline_item("Section 1.1", 1, parent=parent)
writer.add_outline_item("Section 1.2", 5, parent=parent)

with open("with_bookmarks.pdf", "wb") as output_file:
    writer.write(output_file)


# === Créer structure de signets complexe ===

def create_toc_structure(writer, toc_structure):
    """
    Crée une structure de table des matières
    toc_structure: liste de tuples (titre, page, level)
    """
    parents = {}  # Dictionnaire des parents par niveau
    
    for title, page_num, level in toc_structure:
        parent = parents.get(level - 1) if level > 0 else None
        
        bookmark = writer.add_outline_item(
            title, 
            page_num, 
            parent=parent
        )
        
        parents[level] = bookmark

# Exemple de structure
toc = [
    ("Introduction", 0, 0),
    ("Chapitre 1", 5, 0),
    ("  Section 1.1", 6, 1),
    ("  Section 1.2", 10, 1),
    ("    Sous-section 1.2.1", 11, 2),
    ("Chapitre 2", 20, 0),
    ("Conclusion", 50, 0),
]

reader = PdfReader("document.pdf")
writer = PdfWriter()

for page in reader.pages:
    writer.add_page(page)

create_toc_structure(writer, toc)


# === Copier signets d'un PDF existant ===

def copy_outline(reader, writer):
    """Copie la structure des signets"""
    if not reader.outline:
        return
    
    def add_outline_items(outline, parent=None):
        for item in outline:
            if isinstance(item, list):
                add_outline_items(item, parent)
            else:
                title = item.get('/Title', '')
                
                # Trouver la page de destination
                page_num = 0
                if '/Page' in item:
                    page_obj = item['/Page']
                    for i, page in enumerate(reader.pages):
                        if page.indirect_reference == page_obj:
                            page_num = i
                            break
                
                new_item = writer.add_outline_item(
                    title,
                    page_num,
                    parent=parent
                )

    add_outline_items(reader.outline)

# Utilisation
reader = PdfReader("source.pdf")
writer = PdfWriter()

for page in reader.pages:
    writer.add_page(page)

copy_outline(reader, writer)


# === Supprimer tous les signets ===

# Simplement ne pas les ajouter au nouveau PDF
reader = PdfReader("with_bookmarks.pdf")
writer = PdfWriter()

for page in reader.pages:
    writer.add_page(page)

# Pas d'ajout de signets
with open("no_bookmarks.pdf", "wb") as output_file:
    writer.write(output_file)


[OK] ANNOTATIONS


# === Lire les annotations ===

from pypdf import PdfReader

reader = PdfReader("document.pdf")
page = reader.pages[0]

# Obtenir annotations d'une page
if "/Annots" in page:
    annotations = page["/Annots"]
    
    for annot in annotations:
        annot_obj = annot.get_object()
        
        # Type d'annotation
        subtype = annot_obj.get("/Subtype")
        print(f"Type: {subtype}")
        
        # Contenu
        if "/Contents" in annot_obj:
            content = annot_obj["/Contents"]
            print(f"Contenu: {content}")
        
        # Position (rectangle)
        if "/Rect" in annot_obj:
            rect = annot_obj["/Rect"]
            print(f"Position: {rect}")


# === Extraire tous les commentaires ===

def extract_all_annotations(pdf_path):
    """Extrait toutes les annotations du PDF"""
    reader = PdfReader(pdf_path)
    all_annotations = []
    
    for page_num, page in enumerate(reader.pages):
        if "/Annots" not in page:
            continue
        
        annotations = page["/Annots"]
        
        for annot in annotations:
            annot_obj = annot.get_object()
            
            annot_info = {
                'page': page_num + 1,
                'type': str(annot_obj.get("/Subtype", "")),
                'content': str(annot_obj.get("/Contents", "")),
                'author': str(annot_obj.get("/T", "")),  # Title = Author
                'date': str(annot_obj.get("/M", "")),    # Modified date
            }
            
            all_annotations.append(annot_info)
    
    return all_annotations

# Utilisation
annotations = extract_all_annotations("document.pdf")
for annot in annotations:
    print(f"Page {annot['page']}: {annot['type']}")
    print(f"  Auteur: {annot['author']}")
    print(f"  Contenu: {annot['content']}")


# === Ajouter annotations (nécessite annotations module) ===

from pypdf import PdfWriter
from pypdf.annotations import FreeText, Text, Rectangle, Link
from pypdf.generic import RectangleObject

writer = PdfWriter()
reader = PdfReader("document.pdf")

page = reader.pages[0]

# Annotation texte libre
free_text = FreeText(
    text="Ceci est une annotation",
    rect=RectangleObject([100, 100, 300, 150]),
    font="Arial",
    font_size="12pt",
    font_color="ff0000"  # Rouge
)
page.add_annotation(free_text)

# Note adhésive (sticky note)
text_note = Text(
    text="Note importante!",
    rect=RectangleObject([400, 500, 420, 520]),
    open=True
)
page.add_annotation(text_note)

# Rectangle/surlignage
rectangle = Rectangle(
    rect=RectangleObject([50, 50, 200, 100]),
    color="00ff00"  # Vert
)
page.add_annotation(rectangle)

writer.add_page(page)

with open("with_annotations.pdf", "wb") as output_file:
    writer.write(output_file)


# === Ajouter lien hypertexte ===

from pypdf.annotations import Link
from pypdf.generic import RectangleObject

link = Link(
    rect=RectangleObject([100, 700, 200, 720]),
    url="https://example.com",
    border=[0, 0, 0]  # Pas de bordure
)

page.add_annotation(link)


# === Supprimer toutes les annotations ===

def remove_all_annotations(input_pdf, output_pdf):
    """Supprime toutes les annotations"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        # Supprimer les annotations
        if "/Annots" in page:
            del page["/Annots"]
        
        writer.add_page(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


[OK] FORMULAIRES PDF


# === Lire les champs de formulaire ===

from pypdf import PdfReader

reader = PdfReader("formulaire.pdf")

# Obtenir les champs
fields = reader.get_fields()

if fields:
    for field_name, field_data in fields.items():
        print(f"Champ: {field_name}")
        print(f"  Type: {field_data.get('/FT', 'Unknown')}")
        print(f"  Valeur: {field_data.get('/V', '')}")
        print(f"  Options: {field_data.get('/Opt', [])}")


# === Lire valeur d'un champ spécifique ===

def get_form_field_value(reader, field_name):
    """Récupère la valeur d'un champ de formulaire"""
    fields = reader.get_fields()
    if fields and field_name in fields:
        return fields[field_name].get('/V', '')
    return None

# Utilisation
nom = get_form_field_value(reader, "nom")
email = get_form_field_value(reader, "email")


# === Remplir un formulaire ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("formulaire.pdf")
writer = PdfWriter()

# Copier pages
for page in reader.pages:
    writer.add_page(page)

# Remplir les champs
writer.update_page_form_field_values(
    writer.pages[0],
    {
        "nom": "Jean Dupont",
        "email": "jean@example.com",
        "adresse": "123 Rue Example",
        "telephone": "0123456789"
    }
)

with open("formulaire_rempli.pdf", "wb") as output_file:
    writer.write(output_file)


# === Remplir formulaire multi-pages ===

def fill_form(input_pdf, output_pdf, field_values):
    """Remplit un formulaire PDF"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    # Copier toutes les pages
    for page in reader.pages:
        writer.add_page(page)
    
    # Mettre à jour les champs sur chaque page
    for page in writer.pages:
        writer.update_page_form_field_values(page, field_values)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)

# Utilisation
form_data = {
    "nom": "Jean Dupont",
    "prenom": "Jean",
    "email": "jean@example.com",
    "date": "2024-01-15"
}

fill_form("formulaire.pdf", "rempli.pdf", form_data)


# === Aplatir formulaire (rendre non-éditable) ===

def flatten_form(input_pdf, output_pdf):
    """Aplatit le formulaire (valeurs deviennent texte)"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        writer.add_page(page)
    
    # Flatten (selon version pypdf)
    if hasattr(writer, 'flatten_annotations'):
        for page in writer.pages:
            writer.flatten_annotations(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


# === Exporter données de formulaire ===

import json

def export_form_data(pdf_path, output_json):
    """Exporte les données du formulaire en JSON"""
    reader = PdfReader(pdf_path)
    fields = reader.get_fields()
    
    form_data = {}
    if fields:
        for field_name, field_data in fields.items():
            form_data[field_name] = str(field_data.get('/V', ''))
    
    with open(output_json, 'w', encoding='utf-8') as f:
        json.dump(form_data, f, indent=2, ensure_ascii=False)

export_form_data("formulaire_rempli.pdf", "data.json")


[OK] OPTIMISATION ET COMPRESSION


# === Compresser un PDF ===

from pypdf import PdfReader, PdfWriter

reader = PdfReader("document.pdf")
writer = PdfWriter()

# Copier pages
for page in reader.pages:
    writer.add_page(page)

# Compresser
writer.compress_identical_objects()

# Supprimer métadonnées inutiles (optionnel)
writer.add_metadata({"/Producer": ""})

with open("compressed.pdf", "wb") as output_file:
    writer.write(output_file)


# === Compresser avec suppression d'images ===

def compress_pdf_remove_images(input_pdf, output_pdf):
    """Compresse en supprimant les images"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        # Supprimer les images de la page
        if "/XObject" in page["/Resources"]:
            xobjects = page["/Resources"]["/XObject"]
            to_remove = []
            
            for obj_name in xobjects:
                obj = xobjects[obj_name]
                if obj.get_object()["/Subtype"] == "/Image":
                    to_remove.append(obj_name)
            
            for obj_name in to_remove:
                del xobjects[obj_name]
        
        writer.add_page(page)
    
    writer.compress_identical_objects()
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


# === Réduire qualité des images (nécessite PIL) ===

from PIL import Image
import io

def compress_images_in_pdf(input_pdf, output_pdf, quality=50):
    """Réduit la qualité des images dans le PDF"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        if "/XObject" in page["/Resources"]:
            xobjects = page["/Resources"]["/XObject"].get_object()
            
            for obj_name in xobjects:
                obj = xobjects[obj_name]
                
                if obj.get_object()["/Subtype"] == "/Image":
                    try:
                        # Extraire image
                        data = obj.get_data()
                        img = Image.open(io.BytesIO(data))
                        
                        # Compresser
                        img_bytes = io.BytesIO()
                        img.save(img_bytes, format='JPEG', quality=quality)
                        
                        # Remplacer dans PDF (complexe, simplifié ici)
                        # Note: Remplacement réel nécessite manipulation bas niveau
                    except:
                        pass
        
        writer.add_page(page)
    
    writer.compress_identical_objects()
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


# === Supprimer contenu dupliqué ===

def remove_duplicate_content(input_pdf, output_pdf):
    """Supprime objets dupliqués pour réduire taille"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        writer.add_page(page)
    
    # Compression des objets identiques
    writer.compress_identical_objects()
    
    # Supprimer flux inutilisés
    if hasattr(writer, 'remove_unused_objects'):
        writer.remove_unused_objects()
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


[OK] INFORMATION ET ANALYSE


# === Analyser structure du PDF ===

def analyze_pdf(pdf_path):
    """Analyse complète d'un PDF"""
    reader = PdfReader(pdf_path)
    
    analysis = {
        'file_path': pdf_path,
        'file_size': os.path.getsize(pdf_path),
        'num_pages': len(reader.pages),
        'is_encrypted': reader.is_encrypted,
        'metadata': {},
        'pages_info': []
    }
    
    # Métadonnées
    if reader.metadata:
        analysis['metadata'] = {
            'title': reader.metadata.title,
            'author': reader.metadata.author,
            'subject': reader.metadata.subject,
            'creator': reader.metadata.creator,
            'producer': reader.metadata.producer,
        }
    
    # Info par page
    for i, page in enumerate(reader.pages):
        page_info = {
            'page_num': i + 1,
            'width': float(page.mediabox.width),
            'height': float(page.mediabox.height),
            'rotation': page.get("/Rotate", 0),
            'has_annotations': "/Annots" in page,
            'text_length': len(page.extract_text())
        }
        analysis['pages_info'].append(page_info)
    
    return analysis

# Utilisation
import os
import json

info = analyze_pdf("document.pdf")
print(json.dumps(info, indent=2))


# === Compter mots dans PDF ===

def count_words_in_pdf(pdf_path):
    """Compte le nombre de mots dans un PDF"""
    reader = PdfReader(pdf_path)
    total_words = 0
    
    for page in reader.pages:
        text = page.extract_text()
        words = text.split()
        total_words += len(words)
    
    return total_words

word_count = count_words_in_pdf("document.pdf")
print(f"Nombre de mots: {word_count}")


# === Détecter orientation des pages ===

def detect_page_orientations(pdf_path):
    """Détecte l'orientation de chaque page"""
    reader = PdfReader(pdf_path)
    orientations = []
    
    for i, page in enumerate(reader.pages):
        width = float(page.mediabox.width)
        height = float(page.mediabox.height)
        rotation = page.get("/Rotate", 0)
        
        # Déterminer orientation
        if width > height:
            orientation = "Paysage"
        elif width < height:
            orientation = "Portrait"
        else:
            orientation = "Carré"
        
        orientations.append({
            'page': i + 1,
            'orientation': orientation,
            'dimensions': f"{width}x{height}",
            'rotation': rotation
        })
    
    return orientations


# === Vérifier si PDF contient du texte ===

def is_text_pdf(pdf_path, threshold=100):
    """Vérifie si PDF contient du texte extractible"""
    reader = PdfReader(pdf_path)
    
    total_chars = 0
    for page in reader.pages:
        text = page.extract_text()
        total_chars += len(text.strip())
    
    # Si moins de threshold caractères, probablement scanné
    return total_chars > threshold

if is_text_pdf("document.pdf"):
    print("PDF contient du texte extractible")
else:
    print("PDF est probablement scanné (nécessite OCR)")


# === Extraire URLs du PDF ===

import re

def extract_urls_from_pdf(pdf_path):
    """Extrait toutes les URLs du PDF"""
    reader = PdfReader(pdf_path)
    urls = set()
    
    url_pattern = re.compile(r'https?://[^\s<>"]+|www\.[^\s<>"]+')
    
    for page in reader.pages:
        text = page.extract_text()
        found_urls = url_pattern.findall(text)
        urls.update(found_urls)
        
        # URLs dans annotations
        if "/Annots" in page:
            for annot in page["/Annots"]:
                annot_obj = annot.get_object()
                if annot_obj.get("/Subtype") == "/Link":
                    if "/A" in annot_obj:
                        action = annot_obj["/A"]
                        if "/URI" in action:
                            urls.add(str(action["/URI"]))
    
    return list(urls)

urls = extract_urls_from_pdf("document.pdf")
for url in urls:
    print(url)


[OK] CONVERSION ET EXPORT


# === Extraire texte vers fichier ===

def pdf_to_text(pdf_path, output_txt):
    """Extrait tout le texte vers fichier texte"""
    reader = PdfReader(pdf_path)
    
    with open(output_txt, 'w', encoding='utf-8') as f:
        for page_num, page in enumerate(reader.pages):
            f.write(f"{'='*60}\n")
            f.write(f"PAGE {page_num + 1}\n")
            f.write(f"{'='*60}\n\n")
            
            text = page.extract_text()
            f.write(text)
            f.write("\n\n")

pdf_to_text("document.pdf", "output.txt")


# === Extraire texte par page vers fichiers séparés ===

def pdf_to_text_pages(pdf_path, output_folder="text_pages"):
    """Extrait chaque page vers fichier texte séparé"""
    import os
    os.makedirs(output_folder, exist_ok=True)
    
    reader = PdfReader(pdf_path)
    
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text()
        
        output_path = f"{output_folder}/page_{page_num + 1}.txt"
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(text)
        
        print(f"Créé: {output_path}")


# === PDF vers JSON ===

import json

def pdf_to_json(pdf_path, output_json):
    """Exporte PDF vers JSON structuré"""
    reader = PdfReader(pdf_path)
    
    pdf_data = {
        'metadata': {},
        'pages': []
    }
    
    # Métadonnées
    if reader.metadata:
        pdf_data['metadata'] = {
            'title': str(reader.metadata.title or ''),
            'author': str(reader.metadata.author or ''),
            'subject': str(reader.metadata.subject or ''),
        }
    
    # Pages
    for page_num, page in enumerate(reader.pages):
        page_data = {
            'page_number': page_num + 1,
            'text': page.extract_text(),
            'width': float(page.mediabox.width),
            'height': float(page.mediabox.height)
        }
        pdf_data['pages'].append(page_data)
    
    with open(output_json, 'w', encoding='utf-8') as f:
        json.dump(pdf_data, f, indent=2, ensure_ascii=False)

pdf_to_json("document.pdf", "output.json")


# === PDF vers Markdown ===

def pdf_to_markdown(pdf_path, output_md):
    """Convertit PDF vers Markdown (basique)"""
    reader = PdfReader(pdf_path)
    
    with open(output_md, 'w', encoding='utf-8') as f:
        # Titre depuis métadonnées
        if reader.metadata and reader.metadata.title:
            f.write(f"# {reader.metadata.title}\n\n")
            if reader.metadata.author:
                f.write(f"**Auteur:** {reader.metadata.author}\n\n")
            f.write("---\n\n")
        
        # Pages
        for page_num, page in enumerate(reader.pages):
            if page_num > 0:
                f.write("\n\n---\n\n")
            
            f.write(f"## Page {page_num + 1}\n\n")
            text = page.extract_text()
            f.write(text)

pdf_to_markdown("document.pdf", "output.md")


[OK] UTILITAIRES ET FONCTIONS AVANCÉES


# === Réparer PDF corrompu ===

def repair_pdf(input_pdf, output_pdf):
    """Tente de réparer un PDF corrompu"""
    try:
        reader = PdfReader(input_pdf, strict=False)
        writer = PdfWriter()
        
        for page in reader.pages:
            writer.add_page(page)
        
        with open(output_pdf, "wb") as output_file:
            writer.write(output_file)
        
        print("PDF réparé avec succès")
        return True
    except Exception as e:
        print(f"Erreur: {e}")
        return False


# === Comparer deux PDFs ===

def compare_pdfs(pdf1_path, pdf2_path):
    """Compare deux PDFs textuellement"""
    reader1 = PdfReader(pdf1_path)
    reader2 = PdfReader(pdf2_path)
    
    comparison = {
        'same_page_count': len(reader1.pages) == len(reader2.pages),
        'page_count_1': len(reader1.pages),
        'page_count_2': len(reader2.pages),
        'differences': []
    }
    
    max_pages = min(len(reader1.pages), len(reader2.pages))
    
    for i in range(max_pages):
        text1 = reader1.pages[i].extract_text()
        text2 = reader2.pages[i].extract_text()
        
        if text1 != text2:
            comparison['differences'].append({
                'page': i + 1,
                'text1_length': len(text1),
                'text2_length': len(text2)
            })
    
    return comparison


# === Rechercher texte dans PDF ===

def search_in_pdf(pdf_path, search_term, case_sensitive=False):
    """Recherche un terme dans le PDF"""
    reader = PdfReader(pdf_path)
    results = []
    
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text()
        
        if not case_sensitive:
            text_search = text.lower()
            term_search = search_term.lower()
        else:
            text_search = text
            term_search = search_term
        
        if term_search in text_search:
            # Compter occurrences
            count = text_search.count(term_search)
            
            results.append({
                'page': page_num + 1,
                'occurrences': count
            })
    
    return results

# Utilisation
results = search_in_pdf("document.pdf", "important")
for result in results:
    print(f"Page {result['page']}: {result['occurrences']} occurrence(s)")


# === Extraire pages contenant un terme ===

def extract_pages_with_term(input_pdf, output_pdf, search_term):
    """Extrait uniquement les pages contenant un terme"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    pages_found = []
    
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text()
        
        if search_term.lower() in text.lower():
            writer.add_page(page)
            pages_found.append(page_num + 1)
    
    if pages_found:
        with open(output_pdf, "wb") as output_file:
            writer.write(output_file)
        
        print(f"Pages extraites: {pages_found}")
        return pages_found
    else:
        print("Aucune page trouvée")
        return []


# === Fusionner PDFs avec table des matières ===

def merge_with_toc(pdf_list, output_pdf):
    """Fusionne PDFs avec génération de table des matières"""
    merger = PdfMerger()
    
    current_page = 0
    
    for pdf_path in pdf_list:
        reader = PdfReader(pdf_path)
        num_pages = len(reader.pages)
        
        # Extraire nom de fichier pour signet
        import os
        bookmark_title = os.path.splitext(os.path.basename(pdf_path))[0]
        
        # Ajouter avec signet
        merger.append(pdf_path, bookmark=bookmark_title)
        
        current_page += num_pages
    
    merger.write(output_pdf)
    merger.close()
    
    print(f"PDF fusionné avec table des matières: {output_pdf}")


# === Inverser ordre des pages ===

def reverse_pages(input_pdf, output_pdf):
    """Inverse l'ordre des pages"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    # Ajouter pages en ordre inverse
    for page in reversed(reader.pages):
        writer.add_page(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


# === Extraire pages selon critères ===

def extract_pages_by_size(input_pdf, output_pdf, orientation="portrait"):
    """Extrait pages selon orientation"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        width = float(page.mediabox.width)
        height = float(page.mediabox.height)
        
        if orientation == "portrait" and height > width:
            writer.add_page(page)
        elif orientation == "landscape" and width > height:
            writer.add_page(page)
        elif orientation == "square" and width == height:
            writer.add_page(page)
    
    if len(writer.pages) > 0:
        with open(output_pdf, "wb") as output_file:
            writer.write(output_file)
        print(f"Extrait {len(writer.pages)} page(s) {orientation}")


# === Ajouter numéros de page ===

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
import io

def add_page_numbers(input_pdf, output_pdf, position="bottom-right"):
    """Ajoute numéros de page"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page_num, page in enumerate(reader.pages):
        # Créer PDF avec numéro
        packet = io.BytesIO()
        can = canvas.Canvas(packet, pagesize=letter)
        
        # Position du numéro
        width = float(page.mediabox.width)
        height = float(page.mediabox.height)
        
        if position == "bottom-right":
            x, y = width - 50, 20
        elif position == "bottom-center":
            x, y = width / 2, 20
        elif position == "bottom-left":
            x, y = 50, 20
        elif position == "top-right":
            x, y = width - 50, height - 20
        elif position == "top-center":
            x, y = width / 2, height - 20
        else:  # top-left
            x, y = 50, height - 20
        
        can.setFont("Helvetica", 10)
        can.drawString(x, y, str(page_num + 1))
        can.save()
        
        # Fusionner avec page originale
        packet.seek(0)
        number_pdf = PdfReader(packet)
        page.merge_page(number_pdf.pages[0])
        
        writer.add_page(page)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)


# === Supprimer pages vides ===

def remove_blank_pages(input_pdf, output_pdf, threshold=100):
    """Supprime les pages vides (avec peu de texte)"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    removed_pages = []
    
    for page_num, page in enumerate(reader.pages):
        text = page.extract_text().strip()
        
        if len(text) >= threshold:
            writer.add_page(page)
        else:
            removed_pages.append(page_num + 1)
    
    with open(output_pdf, "wb") as output_file:
        writer.write(output_file)
    
    print(f"Pages supprimées: {removed_pages}")
    return removed_pages


# === Créer PDF depuis images ===

from PIL import Image

def images_to_pdf(image_paths, output_pdf):
    """Crée PDF depuis liste d'images"""
    from reportlab.pdfgen import canvas
    from reportlab.lib.utils import ImageReader
    
    # Utiliser première image pour dimensions
    first_img = Image.open(image_paths[0])
    width, height = first_img.size
    
    c = canvas.Canvas(output_pdf, pagesize=(width, height))
    
    for img_path in image_paths:
        img = Image.open(img_path)
        img_width, img_height = img.size
        
        c.setPageSize((img_width, img_height))
        c.drawImage(img_path, 0, 0, img_width, img_height)
        c.showPage()
    
    c.save()
    print(f"PDF créé: {output_pdf}")

# Utilisation
image_list = ["page1.jpg", "page2.jpg", "page3.jpg"]
images_to_pdf(image_list, "from_images.pdf")


# === Extraire page comme image ===

def pdf_page_to_image(pdf_path, page_num, output_image, dpi=300):
    """Convertit une page PDF en image (nécessite pdf2image)"""
    try:
        from pdf2image import convert_from_path
        
        images = convert_from_path(
            pdf_path,
            first_page=page_num,
            last_page=page_num,
            dpi=dpi
        )
        
        if images:
            images[0].save(output_image)
            print(f"Image créée: {output_image}")
    except ImportError:
        print("Installer pdf2image: pip install pdf2image")
        print("Nécessite aussi poppler (système)")


# === Créer miniatures de toutes les pages ===

def create_thumbnails(pdf_path, output_folder="thumbnails", size=(200, 200)):
    """Crée miniatures de toutes les pages"""
    try:
        from pdf2image import convert_from_path
        import os
        
        os.makedirs(output_folder, exist_ok=True)
        
        images = convert_from_path(pdf_path, dpi=72)
        
        for i, image in enumerate(images):
            image.thumbnail(size)
            output_path = f"{output_folder}/page_{i + 1}_thumb.jpg"
            image.save(output_path, "JPEG")
            print(f"Créé: {output_path}")
    except ImportError:
        print("Installer pdf2image: pip install pdf2image")


[OK] GESTION DES ERREURS ET DEBUGGING


# === Gestion robuste des erreurs ===

def safe_pdf_operation(input_pdf, operation_func):
    """Wrapper pour opérations PDF avec gestion erreurs"""
    try:
        reader = PdfReader(input_pdf, strict=False)
        
        # Vérifier si crypté
        if reader.is_encrypted:
            print("PDF crypté. Tentative de décryptage...")
            # Essayer décryptage vide
            if not reader.decrypt(""):
                password = input("Entrez le mot de passe: ")
                if not reader.decrypt(password):
                    raise ValueError("Mot de passe incorrect")
        
        # Exécuter opération
        result = operation_func(reader)
        return result
        
    except Exception as e:
        print(f"Erreur: {type(e).__name__}: {e}")
        return None


# === Vérifier intégrité PDF ===

def verify_pdf_integrity(pdf_path):
    """Vérifie l'intégrité d'un PDF"""
    issues = []
    
    try:
        reader = PdfReader(pdf_path, strict=True)
        
        # Vérifier nombre de pages
        if len(reader.pages) == 0:
            issues.append("PDF sans pages")
        
        # Vérifier chaque page
        for i, page in enumerate(reader.pages):
            try:
                # Tenter extraction texte
                _ = page.extract_text()
                
                # Vérifier dimensions
                if page.mediabox.width <= 0 or page.mediabox.height <= 0:
                    issues.append(f"Page {i+1}: Dimensions invalides")
                    
            except Exception as e:
                issues.append(f"Page {i+1}: Erreur - {e}")
        
        if not issues:
            print("[OK] PDF valide")
            return True
        else:
            print("[X] Problèmes détectés:")
            for issue in issues:
                print(f"  - {issue}")
            return False
            
    except Exception as e:
        print(f"[X] Erreur critique: {e}")
        return False


# === Logger les opérations ===

import logging

def setup_pdf_logger():
    """Configure logger pour opérations PDF"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('pdf_operations.log'),
            logging.StreamHandler()
        ]
    )
    return logging.getLogger('pdf_operations')

# Utilisation
logger = setup_pdf_logger()

def logged_merge_pdfs(pdf_list, output_pdf):
    """Fusion avec logging"""
    logger.info(f"Début fusion de {len(pdf_list)} fichiers")
    
    try:
        merger = PdfMerger()
        
        for pdf in pdf_list:
            logger.info(f"Ajout: {pdf}")
            merger.append(pdf)
        
        merger.write(output_pdf)
        merger.close()
        
        logger.info(f"Fusion réussie: {output_pdf}")
        return True
        
    except Exception as e:
        logger.error(f"Erreur fusion: {e}")
        return False


# === Benchmark performance ===

import time

def benchmark_operation(operation_func, *args, **kwargs):
    """Mesure le temps d'exécution"""
    start_time = time.time()
    
    result = operation_func(*args, **kwargs)
    
    end_time = time.time()
    duration = end_time - start_time
    
    print(f"Opération terminée en {duration:.2f} secondes")
    return result, duration

# Utilisation
result, duration = benchmark_operation(merge_pdfs, pdf_list, "output.pdf")


[OK] EXEMPLES DE SCRIPTS COMPLETS


# === Script CLI pour fusion PDF ===

import argparse
import sys

def cli_merge_pdfs():
    """Script CLI pour fusionner PDFs"""
    parser = argparse.ArgumentParser(description='Fusionner plusieurs PDFs')
    parser.add_argument('files', nargs='+', help='Fichiers PDF à fusionner')
    parser.add_argument('-o', '--output', required=True, help='Fichier de sortie')
    parser.add_argument('-b', '--bookmarks', action='store_true', 
                       help='Ajouter signets')
    
    args = parser.parse_args()
    
    try:
        merger = PdfMerger()
        
        for pdf_file in args.files:
            if args.bookmarks:
                import os
                bookmark = os.path.splitext(os.path.basename(pdf_file))[0]
                merger.append(pdf_file, bookmark=bookmark)
            else:
                merger.append(pdf_file)
        
        merger.write(args.output)
        merger.close()
        
        print(f"[OK] Fusion réussie: {args.output}")
        return 0
        
    except Exception as e:
        print(f"[X] Erreur: {e}", file=sys.stderr)
        return 1

# Utilisation: python script.py file1.pdf file2.pdf -o merged.pdf -b


# === Script pour traitement batch ===

from pathlib import Path
import os

def batch_process_pdfs(input_folder, output_folder, operation):
    """Traite tous les PDFs d'un dossier"""
    os.makedirs(output_folder, exist_ok=True)
    
    pdf_files = list(Path(input_folder).glob("*.pdf"))
    
    print(f"Traitement de {len(pdf_files)} fichiers...")
    
    success_count = 0
    error_count = 0
    
    for pdf_file in pdf_files:
        try:
            output_path = Path(output_folder) / pdf_file.name
            operation(str(pdf_file), str(output_path))
            success_count += 1
            print(f"[OK] {pdf_file.name}")
        except Exception as e:
            error_count += 1
            print(f"[X] {pdf_file.name}: {e}")
    
    print(f"\nRésumé: {success_count} réussis, {error_count} erreurs")

# Exemple: Rotation de tous les PDFs
def rotate_90(input_pdf, output_pdf):
    rotate_all_pages(input_pdf, output_pdf, 90)

batch_process_pdfs("input_pdfs", "output_pdfs", rotate_90)


# === Générateur de rapport PDF ===

def create_pdf_report(pdf_path, output_report):
    """Génère rapport détaillé sur un PDF"""
    reader = PdfReader(pdf_path)
    
    report = []
    report.append("="*60)
    report.append(f"RAPPORT D'ANALYSE PDF")
    report.append("="*60)
    report.append(f"\nFichier: {pdf_path}")
    report.append(f"Taille: {os.path.getsize(pdf_path)} octets")
    report.append(f"\n--- INFORMATIONS GÉNÉRALES ---")
    report.append(f"Nombre de pages: {len(reader.pages)}")
    report.append(f"Crypté: {'Oui' if reader.is_encrypted else 'Non'}")
    
    # Métadonnées
    if reader.metadata:
        report.append(f"\n--- MÉTADONNÉES ---")
        if reader.metadata.title:
            report.append(f"Titre: {reader.metadata.title}")
        if reader.metadata.author:
            report.append(f"Auteur: {reader.metadata.author}")
        if reader.metadata.subject:
            report.append(f"Sujet: {reader.metadata.subject}")
        if reader.metadata.creator:
            report.append(f"Créateur: {reader.metadata.creator}")
    
    # Analyse pages
    report.append(f"\n--- ANALYSE DES PAGES ---")
    
    total_text_length = 0
    landscape_count = 0
    portrait_count = 0
    
    for i, page in enumerate(reader.pages):
        text = page.extract_text()
        text_length = len(text)
        total_text_length += text_length
        
        width = float(page.mediabox.width)
        height = float(page.mediabox.height)
        
        if width > height:
            landscape_count += 1
            orientation = "Paysage"
        else:
            portrait_count += 1
            orientation = "Portrait"
        
        report.append(f"\nPage {i+1}:")
        report.append(f"  Dimensions: {width:.1f} x {height:.1f} points")
        report.append(f"  Orientation: {orientation}")
        report.append(f"  Caractères: {text_length}")
        report.append(f"  Rotation: {page.get('/Rotate', 0)}°")
        report.append(f"  Annotations: {'Oui' if '/Annots' in page else 'Non'}")
    
    # Statistiques
    report.append(f"\n--- STATISTIQUES ---")
    report.append(f"Pages portrait: {portrait_count}")
    report.append(f"Pages paysage: {landscape_count}")
    report.append(f"Caractères totaux: {total_text_length}")
    report.append(f"Moyenne par page: {total_text_length / len(reader.pages):.0f}")
    
    # Signets
    if reader.outline:
        report.append(f"\n--- SIGNETS ---")
        
        def count_bookmarks(outline):
            count = 0
            for item in outline:
                if isinstance(item, list):
                    count += count_bookmarks(item)
                else:
                    count += 1
            return count
        
        bookmark_count = count_bookmarks(reader.outline)
        report.append(f"Nombre de signets: {bookmark_count}")
    
    report.append("\n" + "="*60)
    
    # Écrire rapport
    report_text = "\n".join(report)
    
    with open(output_report, 'w', encoding='utf-8') as f:
        f.write(report_text)
    
    print(report_text)
    print(f"\nRapport sauvegardé: {output_report}")

# Utilisation
create_pdf_report("document.pdf", "rapport.txt")


# === Script d'archivage PDF ===

from datetime import datetime
import shutil

def archive_pdfs(source_folder, archive_folder):
    """Archive PDFs avec date et compression"""
    date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
    archive_path = Path(archive_folder) / f"archive_{date_str}"
    archive_path.mkdir(parents=True, exist_ok=True)
    
    pdf_files = list(Path(source_folder).glob("*.pdf"))
    
    print(f"Archivage de {len(pdf_files)} fichiers...")
    
    for pdf_file in pdf_files:
        try:
            # Copier et compresser
            output_path = archive_path / pdf_file.name
            
            reader = PdfReader(pdf_file)
            writer = PdfWriter()
            
            for page in reader.pages:
                writer.add_page(page)
            
            writer.compress_identical_objects()
            
            with open(output_path, "wb") as f:
                writer.write(f)
            
            print(f"[OK] Archivé: {pdf_file.name}")
            
        except Exception as e:
            print(f"[X] Erreur {pdf_file.name}: {e}")
    
    print(f"\nArchive créée: {archive_path}")


[OK] BONNES PRATIQUES


# === Toujours utiliser context managers ===

# [OK] BON
with open("document.pdf", "rb") as file:
    reader = PdfReader(file)
    # Traitement...

# [X] MAUVAIS (risque fuite ressources)
file = open("document.pdf", "rb")
reader = PdfReader(file)
# Traitement...
# Oubli de file.close()


# === Gestion des erreurs ===

# [OK] BON
try:
    reader = PdfReader("document.pdf")
    text = reader.pages[0].extract_text()
except FileNotFoundError:
    print("Fichier non trouvé")
except Exception as e:
    print(f"Erreur: {e}")

# [X] MAUVAIS
text = reader.pages[0].extract_text()  # Peut crash


# === Vérifier avant traitement ===

# [OK] BON
reader = PdfReader("document.pdf")

if reader.is_encrypted:
    reader.decrypt(password)

if len(reader.pages) > 0:
    text = reader.pages[0].extract_text()

# [X] MAUVAIS (assume PDF valide)
text = reader.pages[0].extract_text()


# === Utiliser strict=False pour PDFs problématiques ===

# Pour PDFs avec erreurs mineures
reader = PdfReader("problematic.pdf", strict=False)


# === Libérer ressources ===

# Pour PdfMerger
merger = PdfMerger()
merger.append("file.pdf")
merger.write("output.pdf")
merger.close()  # Important!

# Ou avec context manager
with PdfMerger() as merger:
    merger.append("file.pdf")
    merger.write("output.pdf")


# === Validation des entrées ===

def safe_extract_page(pdf_path, page_num):
    """Extraction sécurisée de page"""
    if not os.path.exists(pdf_path):
        raise FileNotFoundError(f"PDF non trouvé: {pdf_path}")
    
    reader = PdfReader(pdf_path)
    
    if page_num < 0 or page_num >= len(reader.pages):
        raise ValueError(f"Page {page_num} hors limites (0-{len(reader.pages)-1})")
    
    return reader.pages[page_num].extract_text()


# === Performance: éviter traitements répétés ===

# [OK] BON - Charger une fois
reader = PdfReader("document.pdf")
for page in reader.pages:
    text = page.extract_text()
    # Traiter texte...

# [X] MAUVAIS - Recharge à chaque fois
for i in range(100):
    reader = PdfReader("document.pdf")
    text = reader.pages[i].extract_text()


[OK] RESSOURCES ET RÉFÉRENCES


# Documentation officielle:
# pypdf: https://pypdf.readthedocs.io/
# PyPDF2: https://pypdf2.readthedocs.io/

# Installation complète:
# pip install pypdf[full]
# pip install pypdf[crypto]
# pip install pypdf[image]

# Bibliothèques complémentaires:
# - pdf2image: Conversion PDF vers images
# - pdfplumber: Extraction avancée (tableaux)
# - PyMuPDF (fitz): Alternative performante
# - reportlab: Création PDF from scratch
# - pdfrw: Manipulation bas niveau

# Commandes pip utiles:
# pip install pypdf
# pip install pdf2image
# pip install pdfplumber
# pip install PyMuPDF
# pip install reportlab


[OK] DÉPANNAGE ET FAQ


# === Erreur: "PdfReadError: EOF marker not found" ===
# Solution: Utiliser strict=False
reader = PdfReader("file.pdf", strict=False)


# === Erreur: Texte extrait illisible (caractères bizarres) ===
# Cause: Encodage ou police embedée
# Solution: PDF peut nécessiter OCR si scanné
# Essayer: page.extract_text(extraction_mode="layout")


# === Erreur: Impossible d'extraire images ===
# Vérifier: Images peuvent être dans format propriétaire
# Solution: Utiliser PyMuPDF pour extraction images robuste


# === Performance lente avec gros PDFs ===
# Solutions:
# 1. Traiter par chunks
# 2. Utiliser PyMuPDF (plus rapide)
# 3. Désactiver validations strictes
# 4. Traiter en parallèle avec multiprocessing


# === Mémoire insuffisante ===
# Solution: Traiter page par page plutôt que tout charger
def process_large_pdf(pdf_path):
    reader = PdfReader(pdf_path)
    for i, page in enumerate(reader.pages):
        # Traiter page
        text = page.extract_text()
        # Écrire résultat
        # Page est déchargée après boucle


# === PDF crypté non déchiffrable ===
# Vérifier: Mot de passe correct
# Vérifier: Niveau de cryptage supporté
# Solution: pypdf[crypto] pour cryptage avancé


# === Fusion produit fichier énorme ===
# Solution: Compresser après fusion
merger.write("output.pdf")
# Puis compresser avec writer.compress_identical_objects()


# === Pages blanches après transformation ===
# Cause: Problème de coordinate system
# Solution: Vérifier dimensions et transformations
# Utiliser: page.mediabox pour dimensions


# === Annotations perdues après traitement ===
# Cause: Certaines opérations suppriment annotations
# Solution: Préserver avec flatten_annotations si nécessaire


# === Signets disparaissent après fusion ===
# Solution: Utiliser import_outline=True
merger.append("file.pdf", import_outline=True)


[OK] DIFFERENCES PYPDF vs PyPDF2


# pypdf est le successeur de PyPDF2
# API identique pour compatibilité
# pypdf a corrections bugs et améliorations

# Migration PyPDF2 -> pypdf:
# Remplacer imports:
# from PyPDF2 import PdfReader, PdfWriter
# par:
# from pypdf import PdfReader, PdfWriter

# Tout le reste reste identique!


[OK] EXEMPLES AVANCÉS


# === Pipeline de traitement complet ===

class PDFProcessor:
    """Classe pour pipeline de traitement PDF"""
    
    def __init__(self, input_pdf):
        self.input_pdf = input_pdf
        self.reader = PdfReader(input_pdf)
        self.writer = PdfWriter()
    
    def remove_blank_pages(self, threshold=100):
        """Supprime pages vides"""
        for page in self.reader.pages:
            if len(page.extract_text()) >= threshold:
                self.writer.add_page(page)
        return self
    
    def rotate_landscape(self):
        """Rotation pages paysage"""
        for page in self.writer.pages:
            if page.mediabox.width > page.mediabox.height:
                page.rotate(90)
        return self
    
    def add_watermark(self, watermark_pdf):
        """Ajoute filigrane"""
        watermark = PdfReader(watermark_pdf).pages[0]
        for page in self.writer.pages:
            page.merge_page(watermark)
        return self
    
    def compress(self):
        """Compresse PDF"""
        self.writer.compress_identical_objects()
        return self
    
    def save(self, output_pdf):
        """Sauvegarde résultat"""
        with open(output_pdf, "wb") as f:
            self.writer.write(f)
        return output_pdf

# Utilisation en chaîne
processor = PDFProcessor("document.pdf")
output = (processor
    .remove_blank_pages()
    .rotate_landscape()
    .compress()
    .save("processed.pdf"))

print(f"Traitement terminé: {output}")


# === Traitement parallèle de PDFs ===

from multiprocessing import Pool
import os

def process_single_pdf(args):
    """Traite un seul PDF (pour multiprocessing)"""
    input_pdf, output_folder, operation = args
    
    try:
        output_pdf = os.path.join(output_folder, os.path.basename(input_pdf))
        operation(input_pdf, output_pdf)
        return (input_pdf, True, None)
    except Exception as e:
        return (input_pdf, False, str(e))

def parallel_process_pdfs(pdf_list, output_folder, operation, workers=4):
    """Traite PDFs en parallèle"""
    os.makedirs(output_folder, exist_ok=True)
    
    args_list = [(pdf, output_folder, operation) for pdf in pdf_list]
    
    with Pool(workers) as pool:
        results = pool.map(process_single_pdf, args_list)
    
    # Afficher résultats
    success = sum(1 for _, ok, _ in results if ok)
    failed = len(results) - success
    
    print(f"Terminé: {success} réussis, {failed} échoués")
    
    for pdf, ok, error in results:
        if not ok:
            print(f"[X] {pdf}: {error}")

# Utilisation
pdf_files = ["doc1.pdf", "doc2.pdf", "doc3.pdf"]
parallel_process_pdfs(pdf_files, "output", rotate_all_pages)