Cours de code · auto-généré

Vacances
Tremblant

Générateur de PDF en Python. Un seul fichier, create_pdf.py, qui produit un programme de vacances illustré et stylé.

15 juin 2026 1 fichier · 496 lignes 5 blocs analysés Lecture ~8 min

Navigue avec ou les boutons en bas.

01
Python 3
runtime
02
fpdf2
moteur PDF
03
Pillow · PIL
images + ratio
04
os · stdlib
chemins
05
Arial Unicode
polices TTF
06
~496 LOC
un fichier
Architecture

Trois couches dans un seul fichier

Même sans dossiers, le code se lit en couches. Le quoi (config) est séparé du comment (primitives de rendu), lui-même séparé de l'assemblage (orchestration). C'est ce découpage qui rend le fichier maintenable.

Couche 01

Configuration

Constantes au niveau module : chemins résolus depuis le fichier, polices système et palette de couleurs nommée. Source unique de vérité — un seul endroit à changer pour reskinner tout le PDF.

IMG_DIROUTPUTFONT_REGULARDARK_BLUEACCENT_GREEN
Couche 02

Primitives de rendu

La classe VacationPDF(FPDF) : hooks de page automatiques et méthodes de composant. Un mini-DSL de mise en page construit au-dessus des primitives bas niveau de fpdf.

header()footer()section_title()detail_line()info_box()warning_box()add_image_row()
Couche 03

Orchestration

La fonction create_pdf() assemble le document page par page. Les données (trajets, planning, tarifs) sont des listes ; une boucle les transforme en tableaux.

add_page()trajets[]planning[]rows[]pdf.output()
create_pdf.py · L8–24python
IMG_DIR = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "images")
OUTPUT  = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    "Programme_Vacances_Tremblant.pdf")

FONT_DIR     = "/System/Library/Fonts/Supplemental/"
FONT_REGULAR = os.path.join(FONT_DIR, "Arial Unicode.ttf")
FONT_BOLD    = os.path.join(FONT_DIR, "Arial Bold.ttf")

# Palette nommée = thème unique, réutilisé partout
DARK_BLUE    = (25, 55, 95)
ACCENT_BLUE  = (41, 98, 155)
ACCENT_GREEN = (39, 145, 90)
ACCENT_ORANGE= (230, 126, 34)
LIGHT_GRAY   = (240, 240, 240)
Bloc 01 / 05 Configuration

Chemins ancrés, couleurs nommées

Les chemins sont résolus avec os.path.dirname(os.path.abspath(__file__)), jamais relatifs au répertoire courant. Le script trouve ses images peu importe d'où on le lance. La palette est déclarée une fois en tuples RGB nommés, puis réutilisée par toutes les couches.

Pattern · Configuration centralisée

Constantes au niveau module + ancrage par __file__. Le « thème » (couleurs, polices, chemins) vit en un seul bloc — découplé de la logique qui le consomme.

À retenir

Résous les chemins relatifs au fichier, pas au CWD → script portable. Nomme tes valeurs de thème une fois : un changement, un seul endroit.

create_pdf.py · L28–32python
class VacationPDF(FPDF):
    def setup_fonts(self):
        self.add_font("Main", "",   FONT_REGULAR,     uni=True)
        self.add_font("Main", "B",  FONT_BOLD,        uni=True)
        self.add_font("Main", "I",  FONT_ITALIC,      uni=True)
        self.add_font("Main", "BI", FONT_BOLD_ITALIC, uni=True)

# Ensuite, partout dans le document :
#   self.set_font("Main", "B", 13)
#   self.cell(0, 10, "ÉTÉ 2026 — Famille Maillard")
Bloc 02 / 05 Typographie

Une famille, quatre styles, un alias

Quatre fichiers TTF sont enregistrés sous un seul alias "Main", mappés sur les styles regular / B / I / BI. Le reste du code n'écrit plus jamais de chemin de police : set_font("Main", "B") suffit. Le flag uni=True active l'Unicode — indispensable pour les accents français et les glyphes ✓ ⚠ → utilisés dans les boxes.

Pattern · Enregistrement de ressources

On enregistre une famille de variantes derrière un nom logique. L'appelant raisonne en intention (« gras ») au lieu de manipuler des chemins de fichiers.

À retenir

Un alias + ses variantes = des appels de style propres et sans répétition. Pour du contenu non-ASCII, uni=True n'est pas optionnel.

create_pdf.py · L34–50python
def header(self):
    if self.page_no() > 1:                  # saute la couverture
        self.set_fill_color(*DARK_BLUE)
        self.rect(0, 0, 210, 12, "F")
        self.set_font("Main", "B", 8)
        self.set_text_color(*WHITE)
        self.set_xy(10, 3)
        self.cell(0, 6, "PROGRAMME VACANCES TREMBLANT", align="L")
        self.cell(0, 6, f"Page {self.page_no()}", align="R")

def footer(self):
    self.set_y(-10)
    self.set_font("Main", "I", 7)
    self.set_text_color(150, 150, 150)
    self.cell(0, 10, "Famille Maillard — Tremblant 2026", align="C")
Bloc 03 / 05 Lifecycle

Le framework t'appelle, pas l'inverse

On ne dessine jamais l'en-tête manuellement. fpdf appelle header() et footer() automatiquement à chaque add_page(). Le code n'a qu'à les override. Le garde page_no() > 1 exempte la page de couverture — détail qui montre comment greffer une exception dans un hook sans casser le contrat.

Pattern · Template Method / hooks

Inversion de contrôle : le squelette de rendu de page appartient au framework, tu remplis les trous. Zéro répétition d'en-tête sur 10+ pages.

À retenir

Quand un framework expose des hooks de cycle de vie, override-les plutôt que de répéter le code à chaque itération. Laisse l'appel venir à toi.

create_pdf.py · L77–101python
def detail_line(self, label, value):
    self.set_font("Main", "B", 9)
    label_w = self.get_string_width(label) + 2   # largeur dynamique
    self.cell(label_w, 5, label)
    self.set_font("Main", "", 9)
    self.multi_cell(0, 5, value)

def info_box(self, text, color=ACCENT_GREEN):
    self.set_fill_color(*color)
    self.set_font("Main", "B", 9)
    self.set_text_color(*WHITE)
    self.cell(182, 7, f"  ✓ {text}", fill=True, align="L")
    self.ln(9)

def warning_box(self, text):
    self.set_fill_color(255, 243, 224)
    self.set_draw_color(*ACCENT_ORANGE)
    self.cell(182, 7, f"  ⚠ {text}", fill=True, border=1)
Bloc 04 / 05 Composants UI

Un mini-DSL de mise en page

Chaque motif visuel répété devient une méthode nommée. L'orchestrateur écrit pdf.info_box("Accessible à tous") au lieu de six lignes de set_fill / set_font / cell. detail_line calcule même la largeur du label dynamiquement via get_string_width. Plus loin, add_image_row préserve le ratio (display_h = w*h/w) et se protège avec os.path.exists + try/except.

Pattern · Helpers DSL encapsulés

On construit un vocabulaire métier (box succès, box alerte, ligne label/valeur) au-dessus des primitives brutes. L'intention devient lisible, la répétition disparaît.

À retenir

Encapsule chaque primitive visuelle récurrente en méthode → un mini-DSL. Pour les images, calcule la hauteur d'affichage pour garder le ratio, et garde l'I/O défensive.

create_pdf.py · L198–223python
trajets = [
    ("Croisière Lac Tremblant",    "~15 min"),
    ("Ranch Bel Air (équitation)", "~15-20 min"),
    ("Skyline Luge / Télécabine",  "~20 min"),
    ("Dune Buggy Tours (Labelle)", "~35-40 min"),
    ("Ziptrek Écotour",            "~20 min"),
]

for i, (act, temps) in enumerate(trajets):
    # striping zébré : alterne le fond une ligne sur deux
    pdf.set_fill_color(*(LIGHT_GRAY if i % 2 == 0 else WHITE))
    pdf.set_x(20)
    pdf.cell(col_t[0], 6, act,   border=1, fill=True)
    pdf.cell(col_t[1], 6, temps, border=1, fill=True, align="C")
    pdf.ln(6)
Bloc 05 / 05 Orchestration

Données d'un côté, rendu de l'autre

Le contenu des tableaux vit dans des listes de tuples ; une seule boucle les rend. Ajouter une activité = ajouter une ligne à la liste, sans toucher au code de dessin. Le striping zébré tombe gratuitement avec i % 2. Ce motif se répète pour le planning et le tableau des tarifs — la donnée pilote, la boucle exécute.

Pattern · Rendu data-driven + zebra

Séparation donnée / rendu. La structure de données est la source de vérité ; la présentation est une projection répétable. enumerate + i % 2 = alternance de fond sans état externe.

À retenir

Sépare ce qui change souvent (les données) de ce qui change rarement (la boucle de rendu). Tu étends le contenu en éditant une liste, jamais la logique.

Patterns rencontrés

Six choix d'architecture, et où ils vivent

Les mêmes idées reviennent à travers les couches. Les nommer rend le fichier réutilisable comme modèle pour ton prochain générateur de documents.

Configuration centralisée
Chemins ancrés par __file__ + palette en constantes nommées. Un seul point de vérité pour le thème.
Vu dans · constantes module (L8–24)
Enregistrement de ressources
Famille de polices à 4 variantes derrière un alias unique "Main", avec support Unicode.
Vu dans · setup_fonts() (L28)
Template Method · hooks
fpdf appelle header()/footer() à chaque page. Inversion de contrôle, zéro répétition.
Vu dans · header / footer (L34)
Helpers DSL
Chaque primitive visuelle devient une méthode lisible : info_box, warning_box, detail_line.
Vu dans · méthodes composant (L52–101)
I/O défensive + ratio
os.path.exists + try/except, et hauteur d'image calculée via PIL pour préserver les proportions.
Vu dans · add_image_row() (L65)
Rendu data-driven + zebra
Listes de tuples + boucle enumerate. Le striping zébré sort de i % 2.
Vu dans · tables trajets / tarifs (L198+)
Récapitulatif

Cinq réflexes à emporter

  1. 01Ancre tes chemins sur __file__, jamais sur le répertoire courant — le script reste portable où qu'on le lance.
  2. 02Centralise le thème (couleurs, polices, chemins) en constantes nommées : un changement, un seul endroit à éditer.
  3. 03Quand le framework expose des hooks (header/footer), override-les au lieu de répéter — laisse l'inversion de contrôle travailler.
  4. 04Encapsule chaque motif visuel récurrent en méthode nommée. Tu construis un mini-DSL où l'intention se lit d'un coup d'œil.
  5. 05Sépare données et rendu (liste de tuples + boucle). Étendre le contenu devient éditer une liste, jamais réécrire la logique.