Générateur de PDF en Python. Un seul fichier, create_pdf.py, qui produit un programme de vacances illustré et stylé.
Navigue avec ← → ↑ ↓ ou les boutons en bas.
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.
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.
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.
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.
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)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.
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.
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.
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")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.
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.
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.
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")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.
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.
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.
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)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.
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.
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.
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)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.
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.
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.
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.
__file__, jamais sur le répertoire courant — le script reste portable où qu'on le lance.header/footer), override-les au lieu de répéter — laisse l'inversion de contrôle travailler.