Écrire un générateur PDF en Go pur, sans aucune dépendance
le besoin
Une de mes applications devait produire un bilan mensuel téléchargeable en PDF. Le projet est un mono-binaire Go sans dépendance externe — ajouter une bibliothèque PDF aurait trahi le principe pour une fonctionnalité qui produit du texte mis en page, rien de plus. J'ai voulu voir ce que coûtait le format lui-même.
ce qu'est un PDF, structurellement
Un PDF texte est plus simple qu'il n'y paraît : une suite d'objets numérotés, une table de références (xref) qui donne l'octet de départ de chacun, et une bande-annonce qui pointe vers la racine.
%PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> ← racine 2 0 obj << /Type /Pages /Count 2 /Kids [3 0 R…] >> ← liste des pages 3 0 obj << /Type /Page /Contents 4 0 R … >> ← une page 4 0 obj << /Length 210 >> stream … endstream ← son contenu xref ← offsets de chaque objet trailer << /Size 8 /Root 1 0 R >> startxref %%EOF
Le flux de contenu d'une page est un mini-langage de dessin. Pour du texte :
BT /F1 11 Tf 1 0 0 1 50 780 Tm (Bilan de juillet) Tj ET │ │ │ └ afficher │ │ └ matrice de position (x=50, y=780) │ └ police F1, corps 11 └ début du bloc texte
Les coordonnées partent du coin inférieur gauche, et une page A4 fait 595 × 842 points. La pagination consiste donc à cumuler les hauteurs de ligne et à ouvrir une nouvelle page quand on descend sous la marge basse.
le vrai piege : les accents
Go manipule des chaînes UTF-8. Les polices PDF de base attendent du WinAnsiEncoding — proche de Latin-1. Écrire directement les octets UTF-8 produit un fichier valide où « février » s'affiche « février ».
La conversion est heureusement directe pour le français : les caractères jusqu'à
U+00FF gardent leur valeur. Restent les signes typographiques, qui ont
des positions spécifiques dans WinAnsi :
€ → 0x80 … → 0x85 ' → 0x92 " → 0x93 – → 0x96 — → 0x97 • → 0x95 ° → 0xB0
Sans cette table, chaque puce et chaque tiret cadratin devient un point d'interrogation. Je l'ai découvert en regardant le fichier produit, pas en relisant le code.
verifier autrement qu'a l'oeil
Mes tests unitaires vérifiaient la présence des marqueurs %PDF,
xref, trailer. C'est nécessaire mais très insuffisant : un
fichier peut contenir tous ces marqueurs et rester illisible. J'ai donc généré un
vrai document et l'ai soumis à des outils indépendants :
$ qpdf --check bilan.pdf No syntax or stream encoding errors found $ pdftotext bilan.pdf - | head -3 Bilan de février 2026 Foyer « Chez Jonathan » — Frigo Adaptatif • 187,50 € de courses — sous ton plafond de 250 €
C'est pdftotext qui a révélé les puces transformées en « ? ». Aucun test
unitaire écrit par moi ne l'aurait vu, parce que j'aurais testé ce que je croyais
avoir fait.
le bilan
Environ 180 lignes, zéro dépendance, pagination automatique, gras et corps variables. C'est évidemment limité : pas d'images, pas de tableaux, pas de polices embarquées, pas d'Unicode au-delà de Latin-1. Pour un rapport texte, c'est exactement suffisant.
ce que je retiens
- Lire la spécification d'un format coûte souvent moins cher qu'on ne croit, et rapporte une compréhension qu'aucune bibliothèque ne donne.
- Valider avec un outil qu'on n'a pas écrit. Mes tests validaient mes
propres hypothèses ;
qpdfetpdftotext, non. - « Sans dépendance » est un choix qui se paie et se justifie. Ici le prix était de 180 lignes bien comprises. Pour un PDF avec images et tableaux, j'aurais pris une bibliothèque sans hésiter.