reportlab-layout 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. reportlab_layout-1.0.0/.gitignore +18 -0
  2. reportlab_layout-1.0.0/CHANGELOG.md +72 -0
  3. reportlab_layout-1.0.0/LICENSE +21 -0
  4. reportlab_layout-1.0.0/PKG-INFO +242 -0
  5. reportlab_layout-1.0.0/README.md +209 -0
  6. reportlab_layout-1.0.0/docs/DOC.md +518 -0
  7. reportlab_layout-1.0.0/docs/img/ancrage-cap.png +0 -0
  8. reportlab_layout-1.0.0/docs/img/centrage.png +0 -0
  9. reportlab_layout-1.0.0/examples/attestation.py +85 -0
  10. reportlab_layout-1.0.0/pyproject.toml +93 -0
  11. reportlab_layout-1.0.0/src/reportlab_layout/__init__.py +69 -0
  12. reportlab_layout-1.0.0/src/reportlab_layout/boxes.py +30 -0
  13. reportlab_layout-1.0.0/src/reportlab_layout/colors.py +31 -0
  14. reportlab_layout-1.0.0/src/reportlab_layout/cursor.py +58 -0
  15. reportlab_layout-1.0.0/src/reportlab_layout/document.py +525 -0
  16. reportlab_layout-1.0.0/src/reportlab_layout/frames.py +36 -0
  17. reportlab_layout-1.0.0/src/reportlab_layout/geometry.py +138 -0
  18. reportlab_layout-1.0.0/src/reportlab_layout/images.py +79 -0
  19. reportlab_layout-1.0.0/src/reportlab_layout/metrics.py +204 -0
  20. reportlab_layout-1.0.0/src/reportlab_layout/numbering.py +69 -0
  21. reportlab_layout-1.0.0/src/reportlab_layout/py.typed +0 -0
  22. reportlab_layout-1.0.0/src/reportlab_layout/shapes.py +103 -0
  23. reportlab_layout-1.0.0/src/reportlab_layout/styles.py +84 -0
  24. reportlab_layout-1.0.0/src/reportlab_layout/text.py +80 -0
  25. reportlab_layout-1.0.0/tests/conftest.py +33 -0
  26. reportlab_layout-1.0.0/tests/test_cursor.py +36 -0
  27. reportlab_layout-1.0.0/tests/test_document.py +232 -0
  28. reportlab_layout-1.0.0/tests/test_geometry.py +58 -0
  29. reportlab_layout-1.0.0/tests/test_images.py +52 -0
  30. reportlab_layout-1.0.0/tests/test_metrics.py +129 -0
  31. reportlab_layout-1.0.0/tests/test_numbering.py +57 -0
  32. reportlab_layout-1.0.0/tests/test_shapes.py +56 -0
  33. reportlab_layout-1.0.0/tests/test_styles.py +49 -0
  34. reportlab_layout-1.0.0/tests/test_text.py +55 -0
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
13
+ *.pdf
14
+ !docs/**/*.pdf
15
+ logs/
16
+
17
+ # Ancien pdf_maker, conservé en local le temps de la relecture.
18
+ legacy/
@@ -0,0 +1,72 @@
1
+ # Journal des modifications
2
+
3
+ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le
4
+ versionnage [SemVer](https://semver.org/lang/fr/).
5
+
6
+ ## [1.0.0] — 2026-08-25
7
+
8
+ Première version publiable. Le paquet `pdf_maker`, jusqu'ici module local, est
9
+ découpé, corrigé et renommé `reportlab_layout`. L'API passe en `snake_case` :
10
+ c'est une rupture assumée, la table de correspondance complète est dans
11
+ [`docs/DOC.md`](docs/DOC.md#migration-depuis-pdf_maker).
12
+
13
+ ### Corrigé
14
+
15
+ - **Centrage vertical du texte.** L'ancrage se calculait par
16
+ `y - hauteur/2` avec `hauteur = ascendante - descendante`. La descendante
17
+ étant négative, le texte descendait de `|descendante|` de trop, soit environ
18
+ 20 % du corps. Le décalage correct est `(ascendante + descendante)/2`.
19
+ - **Métriques ignorant l'échelle de tracé.** `drawStringCenterHV` et
20
+ `drawStringLeftCenterV` lisaient la hauteur au corps nominal du style, tandis
21
+ que le texte était dessiné à `fontSize × size` ; `drawStringCenterH` mesurait
22
+ de même la largeur au corps nominal. À `size=0.5`, le centrage était faux d'un
23
+ facteur deux, horizontalement comme verticalement. `TextMetrics` porte
24
+ désormais l'échelle et toutes les métriques en tiennent compte.
25
+ - **`before` interprété dans deux unités.** Il était converti en `unit` pour
26
+ positionner l'élément, mais ajouté en points au curseur, décalant tout ce qui
27
+ suivait.
28
+ - **`valign="top"` de `drawParagraph`.** La hauteur, en points, était ajoutée à
29
+ une ordonnée exprimée en `unit`. Remplacé par `valign` sur `draw()`, appliqué
30
+ en mode absolu.
31
+ - **`addstyle` de `drawTable`.** `TableStyle.add(liste)` empilait la liste comme
32
+ une commande unique, ce qui faisait échouer le rendu du tableau
33
+ (`ValueError: not enough values to unpack`). Les commandes supplémentaires
34
+ passent maintenant par `style=`.
35
+ - **`frameParagraph` levait `TypeError`.** Il transmettait `fontsize=` à
36
+ `getParagraph`, qui n'acceptait pas ce paramètre.
37
+ - **`get_image` sans largeur levait `TypeError`.** Les paramètres `hauteur` et
38
+ `scale` étaient déclarés mais ignorés. `ImageSpec.scaled` les gère et refuse
39
+ explicitement une demande sans contrainte.
40
+ - **`drawParagraph` rendait `(paragraphe, boîte)`**, incohérent avec les autres
41
+ méthodes de tracé, ce qui cassait les appels déballant quatre valeurs. Toutes
42
+ les méthodes rendent désormais une `Box`.
43
+ - **`NumberedCanvas` perdait la dernière page** quand il servait de canvas
44
+ autonome, sans `DocTemplate`.
45
+ - **États de canvas qui fuyaient.** Couleurs, épaisseur de trait et rotation
46
+ n'étaient pas restaurées après un tracé et contaminaient les suivants.
47
+ - **Collision de styles.** La feuille `styles` était un objet de module partagé :
48
+ deux modules définissant le même nom de style levaient `KeyError` à l'import.
49
+ - **Impressions sur la sortie standard.** `newFrame` et `drawFrame` écrivaient
50
+ systématiquement sur `stdout` ; le paquet passe par `logging`.
51
+
52
+ ### Ajouté
53
+
54
+ - `Box`, quadruplet nommé rendu par tous les tracés.
55
+ - `TextMetrics`, métriques d'un style à une échelle donnée, avec les ancrages.
56
+ - `PageGeometry` et `Cursor`, seuls dépositaires des conversions de repères.
57
+ - `draw_string`, méthode unique remplaçant les quatre variantes de tracé de
58
+ chaîne, avec `halign`, `valign`, `angle`, `dx`, `dy`.
59
+ - Ancrage `valign="cap"`, centrage sur la boîte de capitale : sur une étiquette
60
+ courte, l'encre tombe au milieu de sa case à moins de 0,02 pt, et une rangée
61
+ d'étiquettes partage la même ligne de base quels que soient leurs jambages.
62
+ `TextMetrics.cap_height` s'appuie sur `STANDARD_CAP_HEIGHTS`, table reprise
63
+ des fichiers AFM d'Adobe, que reportlab n'expose pas ;
64
+ `scripts/cap_height_probe.py` la revérifie par rastérisation.
65
+ - `ImageSpec`, remplaçant le dictionnaire à clés françaises.
66
+ - `make_stylesheet()` et `add_style()`, pour des feuilles de styles isolées.
67
+ - Gestionnaire de contexte : la sortie n'est écrite que si le bloc réussit.
68
+ - `cursor_y`, `cursor_point`, `remaining_height`, `Cursor.fits`.
69
+ - Prise en charge des noms de format (`"A4"`, `"letter"`) et des couleurs en
70
+ hexadécimal ou en nom CSS.
71
+ - Annotations de types sur toute l'API publique, avec `py.typed`.
72
+ - 124 tests, exécutés sur Python 3.11 à 3.14 et reportlab 4.x et 5.x.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 antnardo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.5
2
+ Name: reportlab_layout
3
+ Version: 1.0.0
4
+ Summary: Une couche de mise en page à curseur au-dessus de reportlab : le flux d'un document, le canvas toujours accessible.
5
+ Project-URL: Homepage, https://github.com/antnardo/reportlab_layout
6
+ Project-URL: Documentation, https://github.com/antnardo/reportlab_layout/blob/main/docs/DOC.md
7
+ Project-URL: Repository, https://github.com/antnardo/reportlab_layout
8
+ Project-URL: Issues, https://github.com/antnardo/reportlab_layout/issues
9
+ Project-URL: Changelog, https://github.com/antnardo/reportlab_layout/blob/main/CHANGELOG.md
10
+ Author-email: antnardo <antnardo@users.noreply.github.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: canvas,layout,pdf,report,reportlab,typesetting
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Natural Language :: French
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Printing
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Topic :: Text Processing :: Markup
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.11
30
+ Requires-Dist: pillow>=10.0
31
+ Requires-Dist: reportlab>=4.0
32
+ Description-Content-Type: text/markdown
33
+
34
+ # reportlab_layout
35
+
36
+ [![CI](https://github.com/antnardo/reportlab_layout/actions/workflows/ci.yml/badge.svg)](https://github.com/antnardo/reportlab_layout/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/reportlab_layout.svg)](https://pypi.org/project/reportlab_layout/)
38
+ [![Python](https://img.shields.io/pypi/pyversions/reportlab_layout.svg)](https://pypi.org/project/reportlab_layout/)
39
+ [![Licence MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](https://github.com/antnardo/reportlab_layout/blob/main/LICENSE)
40
+
41
+ Un curseur qui descend dans la page, et le canvas reportlab resté sous la main.
42
+
43
+ ```bash
44
+ pip install reportlab_layout
45
+ ```
46
+
47
+ ```python
48
+ from reportlab_layout import PDFMaker
49
+
50
+ with PDFMaker("bulletin.pdf", top=25, auto_page_break=True) as doc:
51
+ doc.set_footer(doc.make_paragraph("Établissement X", "Right"))
52
+ doc.draw_paragraph("Bulletin du 3e trimestre", "Heading1 Centered")
53
+ doc.draw_centered_line(wscale=0.4)
54
+ doc.add_space()
55
+ doc.draw_table([["Matière", "Note"], ["Mathématiques", "17"]])
56
+
57
+ # Et au même endroit, sans changer d'outil ni de fichier :
58
+ doc.draw_round_rect(doc.x_left, doc.cursor_y - 40, doc.content_width, 40, radius=6, fill="#eef4fb")
59
+ doc.draw_string("Mention", *doc.cursor_point, halign="left", valign="middle")
60
+ ```
61
+
62
+ ## Le problème que ça règle
63
+
64
+ reportlab offre deux façons de faire, et elles ne se mélangent pas bien.
65
+
66
+ Le **canvas** dessine où vous lui dites, en points, depuis le coin bas-gauche.
67
+ Précis, mais vous tenez vous-même la position courante, vous recalculez les
68
+ hauteurs à la main, et une ligne insérée au milieu décale tout le reste.
69
+
70
+ **Platypus** enchaîne des *flowables* dans des frames et gère le flux, la
71
+ pagination, la coupure des tableaux. Mais il prend la main sur la page : pour
72
+ tracer un cadre au point près derrière un paragraphe, il faut passer par les
73
+ rappels `onPage` d'un `BaseDocTemplate`, c'est-à-dire écrire la mise en page à
74
+ deux endroits différents et dans le désordre.
75
+
76
+ Ce paquet garde les flowables de platypus, mais les pose lui-même à la position
77
+ d'un curseur que vous pouvez lire, déplacer et interroger à tout moment. Le
78
+ canvas reste accessible par `doc.canvas`. Les deux modes se mélangent ligne à
79
+ ligne.
80
+
81
+ ```python
82
+ doc.draw_paragraph("Ce paragraphe descend le curseur.") # flux
83
+ y = doc.cursor_y # position courante
84
+ doc.draw_rect(doc.x_left, y - 30, doc.content_width, 30) # absolu
85
+ doc.advance(30) # le curseur suit
86
+ ```
87
+
88
+ ## Ce qu'il apporte
89
+
90
+ - **Un curseur explicite.** `doc.cursor.depth`, `doc.cursor_y`,
91
+ `doc.remaining_height`, `doc.advance(h)`. Aucune magie : vous savez toujours
92
+ où vous en êtes dans la page.
93
+ - **Une seule méthode pour le texte simple.** `draw_string(texte, x, y,
94
+ halign=…, valign=…, scale=…, angle=…)` remplace la demi-douzaine de variantes
95
+ qu'on finit toujours par écrire, et **le centrage vertical est juste** — voir
96
+ plus bas.
97
+ - **Un retour uniforme.** Chaque tracé rend une `Box(x, y, width, height)`, qui
98
+ se déballe comme un quadruplet. On sait ce qu'on vient de poser et où.
99
+ - **Les deux repères tenus séparément.** L'ordonnée canvas monte, la profondeur
100
+ du curseur descend ; `PageGeometry` est le seul endroit qui convertit.
101
+ - **Rien qui fuit.** Chaque tracé est encadré par `saveState`/`restoreState` :
102
+ une couleur ou une épaisseur de trait ne contamine pas le tracé suivant.
103
+ - **Deux dépendances**, reportlab et Pillow. Ni navigateur sans tête, ni LaTeX,
104
+ ni binaire système.
105
+
106
+ ## Le centrage vertical, puisque c'est le point de départ
107
+
108
+ `canvas.drawString(x, y, texte)` place la **ligne de base** en `y`. Pour centrer
109
+ une chaîne dans une case, la formule qu'on écrit spontanément est
110
+ `y_centre - hauteur/2` avec `hauteur = ascendante - descendante`. Elle est
111
+ fausse : la descendante étant négative, elle descend le texte de `|descendante|`
112
+ de trop, soit environ 20 % du corps. Le bon décalage est
113
+ `y_centre - (ascendante + descendante)/2`.
114
+
115
+ ![Ancienne formule contre valign='middle'](https://raw.githubusercontent.com/antnardo/reportlab_layout/main/docs/img/centrage.png)
116
+
117
+ Le filet rouge marque le milieu exact de la case. À gauche l'ancienne formule, à
118
+ droite `valign="middle"`. En bas la même chose au corps réduit de moitié : la
119
+ version fautive dérive aussi horizontalement, parce que la largeur était mesurée
120
+ au corps nominal alors que le texte était tracé plus petit.
121
+
122
+ ```python
123
+ doc.draw_string("Titre", x, y, halign="center", valign="middle", scale=0.5)
124
+ ```
125
+
126
+ `TextMetrics` porte la correction : toutes ses métriques tiennent compte de
127
+ l'échelle, et `baseline(y, valign)` rend directement la ligne de base voulue.
128
+
129
+ Reste une question que la formule ne tranche pas : centrer **quoi** ?
130
+ `valign="middle"` centre la boîte em, qui réserve la place des jambages même
131
+ quand la chaîne n'en a pas — un libellé comme `DS 3` s'en trouve haut d'environ
132
+ 10 % du corps. `valign="cap"` centre la boîte de capitale : sur une étiquette
133
+ courte, l'encre tombe au milieu de sa case à moins d'un vingtième de point, et
134
+ une rangée d'étiquettes partage la même ligne de base qu'elles aient ou non des
135
+ jambages. C'est l'ancrage des cellules, bandeaux et badges. Le détail chiffré
136
+ est dans [`docs/DOC.md`](https://github.com/antnardo/reportlab_layout/blob/main/docs/DOC.md#middle-ou-cap-).
137
+
138
+ ## Étude comparative
139
+
140
+ ### Le paysage
141
+
142
+ | Outil | Modèle | Dép. système | Licence | Statut (août 2026) |
143
+ | --- | --- | --- | --- | --- |
144
+ | [reportlab](https://pypi.org/project/reportlab/) canvas | absolu, points | non | BSD | 5.0.1, très actif |
145
+ | reportlab platypus | flux, flowables | non | BSD | idem |
146
+ | **`reportlab_layout`** | **curseur + canvas** | **non** | **MIT** | **ce paquet** |
147
+ | [fpdf2](https://pypi.org/project/fpdf2/) | curseur natif | non | LGPL-3.0 | 2.8.8, très actif |
148
+ | [pdfino](https://pypi.org/project/pdfino/) | surcouche platypus | non | MIT | 0.1.0, 2023 |
149
+ | [pdfdocument](https://pypi.org/project/pdfdocument/) | surcouche platypus | non | BSD | 4.0.0, 2020 |
150
+ | [borb](https://pypi.org/project/borb/) | modèle objet | non | AGPL-3.0 | 3.0.9, actif |
151
+ | [WeasyPrint](https://pypi.org/project/weasyprint/) | HTML + CSS | non | BSD | 69.0, très actif |
152
+ | [pdfme](https://pypi.org/project/pdfme/) | document décrit en dict | non | MIT | 0.5.0 |
153
+ | [rst2pdf](https://pypi.org/project/rst2pdf/) | reStructuredText | non | MIT | 0.105, actif |
154
+ | pypdf / pikepdf | manipulation, pas génération | non | BSD / MPL | actifs |
155
+ | pylatex, Typst, wkhtmltopdf | balisage + moteur externe | **oui** | diverses | variés |
156
+
157
+ ### Ce que fait vraiment la concurrence
158
+
159
+ **reportlab platypus** est le concurrent sérieux, et il fait beaucoup plus que
160
+ ce paquet : coupure de tableaux sur plusieurs pages, `KeepTogether`, table des
161
+ matières, signets, gabarits multi-frames. Si votre document est un rapport qui
162
+ coule tout seul du début à la fin, **utilisez platypus** : ce paquet n'a rien à
163
+ lui apporter. La différence tient à un point : dès qu'il faut alterner tracé
164
+ libre et flux dans le même geste, platypus vous impose de séparer le contenu
165
+ (la *story*) de la décoration (les rappels `onPage`). Ici, tout s'écrit dans
166
+ l'ordre où ça se dessine.
167
+
168
+ **fpdf2** est la comparaison la plus honnête, parce que c'est déjà un modèle à
169
+ curseur — `set_xy`, `cell`, `multi_cell`, `ln()` — et qu'il est excellent :
170
+ sous-ensemble HTML, tableaux, signature numérique, communauté vivante. Trois
171
+ différences concrètes. Sa licence est la LGPL-3.0, ce qui suffit à l'écarter
172
+ dans certains contextes ; reportlab est BSD et ce paquet MIT. Son moteur de
173
+ texte riche est un sous-ensemble HTML là où les `Paragraph` de reportlab
174
+ acceptent un balisage propre (`<super>`, `<font>`, indices, puces) et savent se
175
+ replier dans une largeur donnée. Et son curseur *est* l'API : on ne peut pas
176
+ poser un flowable reportlab au milieu. Si vous partez de zéro et que la LGPL ne
177
+ vous gêne pas, fpdf2 est un très bon choix.
178
+
179
+ **pdfino** et **pdfdocument** occupent exactement la même case : une surcouche
180
+ séquentielle au-dessus de platypus. Elles sont plus anciennes et plus simples —
181
+ `h1()`, `p()`, `table()` — et pdfino permet même d'insérer un flowable brut.
182
+ Ce qu'aucune des deux n'offre : la lecture et le déplacement explicites du
183
+ curseur, le placement absolu en points dans la même API, les métriques de
184
+ police corrigées, et une `Box` en retour de chaque tracé. Elles publient un
185
+ document ; celui-ci laisse construire une page. À noter aussi : pdfdocument n'a
186
+ pas publié depuis 2020 et pdfino en est à une 0.1.0 de 2023.
187
+
188
+ **borb** propose un modèle objet complet et lit aussi les PDF existants. C'est
189
+ plus ambitieux ; c'est aussi de l'AGPL-3.0, une licence qui contamine tout
190
+ service qui l'expose sur le réseau. À écarter d'emblée en contexte propriétaire.
191
+
192
+ **WeasyPrint** produit la plus belle typographie de la liste et gère le CSS
193
+ paginé (`@page`, en-têtes courants, coupures). Le prix à payer : il faut
194
+ exprimer la mise en page en HTML et CSS, et le rendu passe par un moteur de
195
+ rendu complet. Pour un document dont la géométrie se calcule — un planning
196
+ annuel, un tableau de créneaux, une grille de photos — décrire des coordonnées
197
+ en CSS est un détour. Pour une facture ou un rapport dont vous avez déjà le
198
+ gabarit web, WeasyPrint gagne largement.
199
+
200
+ **pdfme**, **rst2pdf**, **pylatex** partagent le même parti : décrire le
201
+ document dans un format (dict, reStructuredText, LaTeX) et laisser un moteur le
202
+ composer. Excellent quand le contenu est le sujet ; inadapté quand ce sont les
203
+ coordonnées qui le sont. pylatex et Typst ajoutent en plus une dépendance
204
+ système lourde.
205
+
206
+ **pypdf** et **pikepdf** ne génèrent pas de document : ils fusionnent, découpent
207
+ et signent. Complémentaires, pas concurrents.
208
+
209
+ ### En résumé
210
+
211
+ | Votre besoin | Préférez |
212
+ | --- | --- |
213
+ | Un rapport qui coule tout seul, tableaux coupés sur plusieurs pages, table des matières | reportlab platypus |
214
+ | Le gabarit existe déjà en HTML/CSS | WeasyPrint |
215
+ | Un curseur simple, sans reportlab, et la LGPL ne pose pas problème | fpdf2 |
216
+ | Publier un document simple en quelques appels | pdfino |
217
+ | Découper, fusionner ou signer des PDF existants | pypdf, pikepdf |
218
+ | Le contenu est un texte balisé | rst2pdf, pylatex |
219
+ | **Une page dont la géométrie se calcule, en alternant flux et tracé au point près** | **`reportlab_layout`** |
220
+
221
+ Ce paquet occupe une case étroite : les documents où l'on connaît la formule qui
222
+ donne la position de chaque élément — plannings, grilles, calendriers, tableaux
223
+ de créneaux, planches — et où l'on veut quand même écrire du texte au fil de
224
+ l'eau sans compter les points à la main.
225
+
226
+ ## Aller plus loin
227
+
228
+ - [`docs/DOC.md`](https://github.com/antnardo/reportlab_layout/blob/main/docs/DOC.md) — référence complète de l'API, repères,
229
+ positionnement, styles, métriques, frames, pagination.
230
+ - [`examples/attestation.py`](https://github.com/antnardo/reportlab_layout/blob/main/examples/attestation.py) — un document d'une page,
231
+ flux et tracé absolu mêlés.
232
+ - [`scripts/centering_proof.py`](https://github.com/antnardo/reportlab_layout/blob/main/scripts/centering_proof.py) — la preuve
233
+ chiffrée et visuelle du centrage.
234
+
235
+ ## Compatibilité
236
+
237
+ Python 3.11 à 3.14, reportlab 4.x et 5.x. La matrice d'intégration continue
238
+ couvre chacune de ces huit combinaisons.
239
+
240
+ ## Licence
241
+
242
+ MIT. reportlab est sous licence BSD, Pillow sous licence MIT-CMU.
@@ -0,0 +1,209 @@
1
+ # reportlab_layout
2
+
3
+ [![CI](https://github.com/antnardo/reportlab_layout/actions/workflows/ci.yml/badge.svg)](https://github.com/antnardo/reportlab_layout/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/reportlab_layout.svg)](https://pypi.org/project/reportlab_layout/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/reportlab_layout.svg)](https://pypi.org/project/reportlab_layout/)
6
+ [![Licence MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](https://github.com/antnardo/reportlab_layout/blob/main/LICENSE)
7
+
8
+ Un curseur qui descend dans la page, et le canvas reportlab resté sous la main.
9
+
10
+ ```bash
11
+ pip install reportlab_layout
12
+ ```
13
+
14
+ ```python
15
+ from reportlab_layout import PDFMaker
16
+
17
+ with PDFMaker("bulletin.pdf", top=25, auto_page_break=True) as doc:
18
+ doc.set_footer(doc.make_paragraph("Établissement X", "Right"))
19
+ doc.draw_paragraph("Bulletin du 3e trimestre", "Heading1 Centered")
20
+ doc.draw_centered_line(wscale=0.4)
21
+ doc.add_space()
22
+ doc.draw_table([["Matière", "Note"], ["Mathématiques", "17"]])
23
+
24
+ # Et au même endroit, sans changer d'outil ni de fichier :
25
+ doc.draw_round_rect(doc.x_left, doc.cursor_y - 40, doc.content_width, 40, radius=6, fill="#eef4fb")
26
+ doc.draw_string("Mention", *doc.cursor_point, halign="left", valign="middle")
27
+ ```
28
+
29
+ ## Le problème que ça règle
30
+
31
+ reportlab offre deux façons de faire, et elles ne se mélangent pas bien.
32
+
33
+ Le **canvas** dessine où vous lui dites, en points, depuis le coin bas-gauche.
34
+ Précis, mais vous tenez vous-même la position courante, vous recalculez les
35
+ hauteurs à la main, et une ligne insérée au milieu décale tout le reste.
36
+
37
+ **Platypus** enchaîne des *flowables* dans des frames et gère le flux, la
38
+ pagination, la coupure des tableaux. Mais il prend la main sur la page : pour
39
+ tracer un cadre au point près derrière un paragraphe, il faut passer par les
40
+ rappels `onPage` d'un `BaseDocTemplate`, c'est-à-dire écrire la mise en page à
41
+ deux endroits différents et dans le désordre.
42
+
43
+ Ce paquet garde les flowables de platypus, mais les pose lui-même à la position
44
+ d'un curseur que vous pouvez lire, déplacer et interroger à tout moment. Le
45
+ canvas reste accessible par `doc.canvas`. Les deux modes se mélangent ligne à
46
+ ligne.
47
+
48
+ ```python
49
+ doc.draw_paragraph("Ce paragraphe descend le curseur.") # flux
50
+ y = doc.cursor_y # position courante
51
+ doc.draw_rect(doc.x_left, y - 30, doc.content_width, 30) # absolu
52
+ doc.advance(30) # le curseur suit
53
+ ```
54
+
55
+ ## Ce qu'il apporte
56
+
57
+ - **Un curseur explicite.** `doc.cursor.depth`, `doc.cursor_y`,
58
+ `doc.remaining_height`, `doc.advance(h)`. Aucune magie : vous savez toujours
59
+ où vous en êtes dans la page.
60
+ - **Une seule méthode pour le texte simple.** `draw_string(texte, x, y,
61
+ halign=…, valign=…, scale=…, angle=…)` remplace la demi-douzaine de variantes
62
+ qu'on finit toujours par écrire, et **le centrage vertical est juste** — voir
63
+ plus bas.
64
+ - **Un retour uniforme.** Chaque tracé rend une `Box(x, y, width, height)`, qui
65
+ se déballe comme un quadruplet. On sait ce qu'on vient de poser et où.
66
+ - **Les deux repères tenus séparément.** L'ordonnée canvas monte, la profondeur
67
+ du curseur descend ; `PageGeometry` est le seul endroit qui convertit.
68
+ - **Rien qui fuit.** Chaque tracé est encadré par `saveState`/`restoreState` :
69
+ une couleur ou une épaisseur de trait ne contamine pas le tracé suivant.
70
+ - **Deux dépendances**, reportlab et Pillow. Ni navigateur sans tête, ni LaTeX,
71
+ ni binaire système.
72
+
73
+ ## Le centrage vertical, puisque c'est le point de départ
74
+
75
+ `canvas.drawString(x, y, texte)` place la **ligne de base** en `y`. Pour centrer
76
+ une chaîne dans une case, la formule qu'on écrit spontanément est
77
+ `y_centre - hauteur/2` avec `hauteur = ascendante - descendante`. Elle est
78
+ fausse : la descendante étant négative, elle descend le texte de `|descendante|`
79
+ de trop, soit environ 20 % du corps. Le bon décalage est
80
+ `y_centre - (ascendante + descendante)/2`.
81
+
82
+ ![Ancienne formule contre valign='middle'](https://raw.githubusercontent.com/antnardo/reportlab_layout/main/docs/img/centrage.png)
83
+
84
+ Le filet rouge marque le milieu exact de la case. À gauche l'ancienne formule, à
85
+ droite `valign="middle"`. En bas la même chose au corps réduit de moitié : la
86
+ version fautive dérive aussi horizontalement, parce que la largeur était mesurée
87
+ au corps nominal alors que le texte était tracé plus petit.
88
+
89
+ ```python
90
+ doc.draw_string("Titre", x, y, halign="center", valign="middle", scale=0.5)
91
+ ```
92
+
93
+ `TextMetrics` porte la correction : toutes ses métriques tiennent compte de
94
+ l'échelle, et `baseline(y, valign)` rend directement la ligne de base voulue.
95
+
96
+ Reste une question que la formule ne tranche pas : centrer **quoi** ?
97
+ `valign="middle"` centre la boîte em, qui réserve la place des jambages même
98
+ quand la chaîne n'en a pas — un libellé comme `DS 3` s'en trouve haut d'environ
99
+ 10 % du corps. `valign="cap"` centre la boîte de capitale : sur une étiquette
100
+ courte, l'encre tombe au milieu de sa case à moins d'un vingtième de point, et
101
+ une rangée d'étiquettes partage la même ligne de base qu'elles aient ou non des
102
+ jambages. C'est l'ancrage des cellules, bandeaux et badges. Le détail chiffré
103
+ est dans [`docs/DOC.md`](https://github.com/antnardo/reportlab_layout/blob/main/docs/DOC.md#middle-ou-cap-).
104
+
105
+ ## Étude comparative
106
+
107
+ ### Le paysage
108
+
109
+ | Outil | Modèle | Dép. système | Licence | Statut (août 2026) |
110
+ | --- | --- | --- | --- | --- |
111
+ | [reportlab](https://pypi.org/project/reportlab/) canvas | absolu, points | non | BSD | 5.0.1, très actif |
112
+ | reportlab platypus | flux, flowables | non | BSD | idem |
113
+ | **`reportlab_layout`** | **curseur + canvas** | **non** | **MIT** | **ce paquet** |
114
+ | [fpdf2](https://pypi.org/project/fpdf2/) | curseur natif | non | LGPL-3.0 | 2.8.8, très actif |
115
+ | [pdfino](https://pypi.org/project/pdfino/) | surcouche platypus | non | MIT | 0.1.0, 2023 |
116
+ | [pdfdocument](https://pypi.org/project/pdfdocument/) | surcouche platypus | non | BSD | 4.0.0, 2020 |
117
+ | [borb](https://pypi.org/project/borb/) | modèle objet | non | AGPL-3.0 | 3.0.9, actif |
118
+ | [WeasyPrint](https://pypi.org/project/weasyprint/) | HTML + CSS | non | BSD | 69.0, très actif |
119
+ | [pdfme](https://pypi.org/project/pdfme/) | document décrit en dict | non | MIT | 0.5.0 |
120
+ | [rst2pdf](https://pypi.org/project/rst2pdf/) | reStructuredText | non | MIT | 0.105, actif |
121
+ | pypdf / pikepdf | manipulation, pas génération | non | BSD / MPL | actifs |
122
+ | pylatex, Typst, wkhtmltopdf | balisage + moteur externe | **oui** | diverses | variés |
123
+
124
+ ### Ce que fait vraiment la concurrence
125
+
126
+ **reportlab platypus** est le concurrent sérieux, et il fait beaucoup plus que
127
+ ce paquet : coupure de tableaux sur plusieurs pages, `KeepTogether`, table des
128
+ matières, signets, gabarits multi-frames. Si votre document est un rapport qui
129
+ coule tout seul du début à la fin, **utilisez platypus** : ce paquet n'a rien à
130
+ lui apporter. La différence tient à un point : dès qu'il faut alterner tracé
131
+ libre et flux dans le même geste, platypus vous impose de séparer le contenu
132
+ (la *story*) de la décoration (les rappels `onPage`). Ici, tout s'écrit dans
133
+ l'ordre où ça se dessine.
134
+
135
+ **fpdf2** est la comparaison la plus honnête, parce que c'est déjà un modèle à
136
+ curseur — `set_xy`, `cell`, `multi_cell`, `ln()` — et qu'il est excellent :
137
+ sous-ensemble HTML, tableaux, signature numérique, communauté vivante. Trois
138
+ différences concrètes. Sa licence est la LGPL-3.0, ce qui suffit à l'écarter
139
+ dans certains contextes ; reportlab est BSD et ce paquet MIT. Son moteur de
140
+ texte riche est un sous-ensemble HTML là où les `Paragraph` de reportlab
141
+ acceptent un balisage propre (`<super>`, `<font>`, indices, puces) et savent se
142
+ replier dans une largeur donnée. Et son curseur *est* l'API : on ne peut pas
143
+ poser un flowable reportlab au milieu. Si vous partez de zéro et que la LGPL ne
144
+ vous gêne pas, fpdf2 est un très bon choix.
145
+
146
+ **pdfino** et **pdfdocument** occupent exactement la même case : une surcouche
147
+ séquentielle au-dessus de platypus. Elles sont plus anciennes et plus simples —
148
+ `h1()`, `p()`, `table()` — et pdfino permet même d'insérer un flowable brut.
149
+ Ce qu'aucune des deux n'offre : la lecture et le déplacement explicites du
150
+ curseur, le placement absolu en points dans la même API, les métriques de
151
+ police corrigées, et une `Box` en retour de chaque tracé. Elles publient un
152
+ document ; celui-ci laisse construire une page. À noter aussi : pdfdocument n'a
153
+ pas publié depuis 2020 et pdfino en est à une 0.1.0 de 2023.
154
+
155
+ **borb** propose un modèle objet complet et lit aussi les PDF existants. C'est
156
+ plus ambitieux ; c'est aussi de l'AGPL-3.0, une licence qui contamine tout
157
+ service qui l'expose sur le réseau. À écarter d'emblée en contexte propriétaire.
158
+
159
+ **WeasyPrint** produit la plus belle typographie de la liste et gère le CSS
160
+ paginé (`@page`, en-têtes courants, coupures). Le prix à payer : il faut
161
+ exprimer la mise en page en HTML et CSS, et le rendu passe par un moteur de
162
+ rendu complet. Pour un document dont la géométrie se calcule — un planning
163
+ annuel, un tableau de créneaux, une grille de photos — décrire des coordonnées
164
+ en CSS est un détour. Pour une facture ou un rapport dont vous avez déjà le
165
+ gabarit web, WeasyPrint gagne largement.
166
+
167
+ **pdfme**, **rst2pdf**, **pylatex** partagent le même parti : décrire le
168
+ document dans un format (dict, reStructuredText, LaTeX) et laisser un moteur le
169
+ composer. Excellent quand le contenu est le sujet ; inadapté quand ce sont les
170
+ coordonnées qui le sont. pylatex et Typst ajoutent en plus une dépendance
171
+ système lourde.
172
+
173
+ **pypdf** et **pikepdf** ne génèrent pas de document : ils fusionnent, découpent
174
+ et signent. Complémentaires, pas concurrents.
175
+
176
+ ### En résumé
177
+
178
+ | Votre besoin | Préférez |
179
+ | --- | --- |
180
+ | Un rapport qui coule tout seul, tableaux coupés sur plusieurs pages, table des matières | reportlab platypus |
181
+ | Le gabarit existe déjà en HTML/CSS | WeasyPrint |
182
+ | Un curseur simple, sans reportlab, et la LGPL ne pose pas problème | fpdf2 |
183
+ | Publier un document simple en quelques appels | pdfino |
184
+ | Découper, fusionner ou signer des PDF existants | pypdf, pikepdf |
185
+ | Le contenu est un texte balisé | rst2pdf, pylatex |
186
+ | **Une page dont la géométrie se calcule, en alternant flux et tracé au point près** | **`reportlab_layout`** |
187
+
188
+ Ce paquet occupe une case étroite : les documents où l'on connaît la formule qui
189
+ donne la position de chaque élément — plannings, grilles, calendriers, tableaux
190
+ de créneaux, planches — et où l'on veut quand même écrire du texte au fil de
191
+ l'eau sans compter les points à la main.
192
+
193
+ ## Aller plus loin
194
+
195
+ - [`docs/DOC.md`](https://github.com/antnardo/reportlab_layout/blob/main/docs/DOC.md) — référence complète de l'API, repères,
196
+ positionnement, styles, métriques, frames, pagination.
197
+ - [`examples/attestation.py`](https://github.com/antnardo/reportlab_layout/blob/main/examples/attestation.py) — un document d'une page,
198
+ flux et tracé absolu mêlés.
199
+ - [`scripts/centering_proof.py`](https://github.com/antnardo/reportlab_layout/blob/main/scripts/centering_proof.py) — la preuve
200
+ chiffrée et visuelle du centrage.
201
+
202
+ ## Compatibilité
203
+
204
+ Python 3.11 à 3.14, reportlab 4.x et 5.x. La matrice d'intégration continue
205
+ couvre chacune de ces huit combinaisons.
206
+
207
+ ## Licence
208
+
209
+ MIT. reportlab est sous licence BSD, Pillow sous licence MIT-CMU.