blog-image 0.1.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.
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Discuter sur Discord
4
+ url: https://discord.gg/FS2xqJ4QT6
5
+ about: Le salon #blog-image du serveur F-Overseas Solutions — la réponse y arrive plus vite que dans une issue.
6
+ - name: Question d'usage (Q&A)
7
+ url: https://github.com/foverseas/blog-image/discussions/categories/q-a
8
+ about: Comment choisir ses formats, une police qui ne se charge pas, un titre qui se coupe mal — une réponse marquée ici sert au suivant.
@@ -0,0 +1,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+
15
+ - uses: astral-sh/setup-uv@v10.3.2
16
+ with:
17
+ python-version: "3.12"
18
+ enable-cache: true
19
+
20
+ - name: Install the project
21
+ run: uv sync --locked --extra dev
22
+
23
+ - name: Lint
24
+ run: uv run ruff check .
25
+
26
+ tests:
27
+ runs-on: ubuntu-latest
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ python-version: ["3.10", "3.12", "3.14"]
32
+
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+
36
+ # resolve_fonts() falls back to DejaVu when the spec names no family;
37
+ # the runner image is not guaranteed to carry it.
38
+ - name: Install the fallback font family
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y --no-install-recommends fonts-dejavu-core
42
+
43
+ - uses: astral-sh/setup-uv@v10.3.2
44
+ with:
45
+ python-version: ${{ matrix.python-version }}
46
+ enable-cache: true
47
+
48
+ - name: Install the project
49
+ run: uv sync --locked --extra dev
50
+
51
+ - name: Run the tests
52
+ run: uv run pytest -q
@@ -0,0 +1,42 @@
1
+ name: Publier sur PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ # L'authentification auprès de PyPI se fait par ce jeton OIDC,
13
+ # pas par un secret : il n'y a aucun token à stocker ici.
14
+ id-token: write
15
+ contents: read
16
+
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+
20
+ - uses: astral-sh/setup-uv@v10.3.2
21
+ with:
22
+ python-version: "3.12"
23
+ enable-cache: true
24
+
25
+ # Un tag qui ne correspond pas à la version du paquet publierait un
26
+ # numéro inattendu, et PyPI n'accepte jamais deux fois la même version.
27
+ - name: Vérifier que le tag correspond à la version du paquet
28
+ run: |
29
+ tag="${GITHUB_REF_NAME#v}"
30
+ version="$(uv version --short)"
31
+ if [ "$tag" != "$version" ]; then
32
+ echo "::error::le tag $GITHUB_REF_NAME annonce $tag, pyproject.toml dit $version"
33
+ exit 1
34
+ fi
35
+
36
+ - name: Construire la distribution
37
+ run: uv build
38
+
39
+ - name: Vérifier les métadonnées
40
+ run: uvx twine check dist/*
41
+
42
+ - uses: pypa/gh-action-pypi-publish@v1.14.3
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .DS_Store
10
+
11
+ # generated cards
12
+ out/
13
+ *.png
14
+ !docs/*.png
@@ -0,0 +1,42 @@
1
+ # Journal des changements
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
+ ## [0.1.0] - 2026-10-24
7
+
8
+ Première version.
9
+
10
+ ### Ajouté
11
+
12
+ - Génération des images de couverture d'un article dans plusieurs rapports
13
+ d'image à partir d'un fichier YAML, pour ne plus entretenir à la main trois
14
+ fichiers qui divergent au premier changement de titre.
15
+ - Trois formats par défaut — 16:9 (1600×900) pour Open Graph et l'en-tête du
16
+ billet, 4:3 (1600×1200) pour l'emplacement Discover, 1:1 (1200×1200) pour les
17
+ recadrages sociaux. Les ratios `3x2`, `2x1`, `4x5` et `9x16` sont connus, et
18
+ toute taille s'écrit en pixels (`1240x700`).
19
+ - Clés de description en français ou en anglais (`titre` ou `title`, `fond` ou
20
+ `background`). Une clé inconnue est une erreur plutôt qu'un silence.
21
+ - Coupe automatique du titre sur la largeur mesurée de la police, avec
22
+ réduction du corps tant que le compte de lignes ou la largeur dépasse. Une
23
+ seule description sert les trois formats.
24
+ - Respect des espaces insécables : un `?` ou un `:` français ne part pas seul à
25
+ la ligne.
26
+ - Motifs `particules` et `grille`, ou `aucun`, dimensionnés sur l'unité
27
+ typographique et non sur la bande qu'ils occupent.
28
+ - Résolution de police multi-plateforme : une famille, un gabarit de chemin
29
+ contenant `%s`, ou une paire de fichiers explicite. À défaut, repli sur IBM
30
+ Plex Mono, DejaVu puis Arial, avec un message qui dit où l'outil a cherché.
31
+ - Interface en ligne de commande (`-o`, `-f`, `-n`, `--dry-run`, `-q`) et API
32
+ Python (`load_spec`, `resolve_fonts`, `render`).
33
+
34
+ ### Notes
35
+
36
+ - Le rendu est reproductible : les mêmes descriptions produisent les mêmes
37
+ fichiers, octet pour octet, de Pillow 11.3 à Pillow 13.0.
38
+ - 29 tests, exécutés en intégration continue sur Python 3.10, 3.12 et 3.14.
39
+ - Le paquet n'est pas publié sur PyPI ; il s'installe depuis ce dépôt avec
40
+ `uv tool install git+https://github.com/foverseas/blog-image`.
41
+
42
+ [0.1.0]: https://github.com/foverseas/blog-image/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 F-Overseas Solutions
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,190 @@
1
+ Metadata-Version: 2.5
2
+ Name: blog-image
3
+ Version: 0.1.0
4
+ Summary: Generate article cover images in several aspect ratios from a short YAML file.
5
+ Project-URL: Homepage, https://github.com/foverseas/blog-image
6
+ Project-URL: Repository, https://github.com/foverseas/blog-image
7
+ Project-URL: Issues, https://github.com/foverseas/blog-image/issues
8
+ Project-URL: Discord, https://discord.gg/FS2xqJ4QT6
9
+ Author: F-Overseas Solutions
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: blog,cover-image,og-image,pillow,social-card
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Multimedia :: Graphics
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: pillow>=10.0
20
+ Requires-Dist: pyyaml>=6.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0; extra == 'dev'
23
+ Requires-Dist: ruff>=0.17; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # blog-image
27
+
28
+ [![CI](https://github.com/foverseas/blog-image/actions/workflows/ci.yml/badge.svg)](https://github.com/foverseas/blog-image/actions/workflows/ci.yml)
29
+
30
+ Génère les images de couverture d'un article dans plusieurs formats, à partir
31
+ d'un fichier YAML de trois lignes.
32
+
33
+ ```yaml
34
+ # image.yml
35
+ titre: "Combien coûte une application mobile"
36
+ accent: "#00ff99"
37
+ fond: "#1c1c22"
38
+ ```
39
+
40
+ ```console
41
+ $ blog-image image.yml
42
+ -> article-16x9.png 1600x900
43
+ -> article-4x3.png 1600x1200
44
+ -> article-1x1.png 1200x1200
45
+ ```
46
+
47
+ ![Exemple de carte 16:9](https://raw.githubusercontent.com/foverseas/blog-image/v0.1.0/docs/exemple-16x9.png)
48
+
49
+ Trois formats parce qu'un article en a besoin de trois : le 16:9 pour Open
50
+ Graph et l'en-tête du billet, le 4:3 pour l'emplacement Discover, le 1:1 pour
51
+ les recadrages sociaux. Les générer séparément, c'est trois fichiers qui
52
+ divergent au premier changement de titre.
53
+
54
+ ## Installation
55
+
56
+ Avec [uv](https://docs.astral.sh/uv/), sans rien installer de permanent :
57
+
58
+ ```console
59
+ $ uvx --from blog-image blog-image image.yml
60
+ ```
61
+
62
+ Pour l'avoir sous la main partout :
63
+
64
+ ```console
65
+ $ uv tool install blog-image # ou : pipx install blog-image
66
+ ```
67
+
68
+ Et en développement, depuis les sources :
69
+
70
+ ```console
71
+ $ git clone https://github.com/foverseas/blog-image
72
+ $ cd blog-image
73
+ $ uv sync --extra dev # crée .venv d'après uv.lock
74
+ $ uv run blog-image examples/image.yml
75
+ ```
76
+
77
+ `pip install blog-image` fonctionne aussi : c'est un paquet ordinaire, uv n'est
78
+ pas une dépendance.
79
+
80
+ Il faut Python 3.10, Pillow et PyYAML. La police par défaut est JetBrains
81
+ Mono ; si elle n'est pas installée, le programme retombe sur IBM Plex Mono,
82
+ DejaVu puis Arial, et dit où il a cherché s'il ne trouve rien.
83
+
84
+ ## Le fichier de description
85
+
86
+ Seul `titre` est obligatoire. Les clés s'écrivent en français ou en anglais.
87
+
88
+ | Clé | Alias | Défaut | Rôle |
89
+ | --- | --- | --- | --- |
90
+ | `titre` | `title` | — | le titre, coupé automatiquement sur la largeur réelle de la police |
91
+ | `sous-titre` | `subtitle` | vide | une ligne sous le titre ; une liste devient autant de lignes |
92
+ | `chapeau` | `eyebrow` | vide | surtitre en capitales espacées, dans la couleur d'accent |
93
+ | `pied` | `footer` | vide | le pied de carte, précédé d'une pastille |
94
+ | `fond` | `background` | `#1c1c22` | fond de la carte |
95
+ | `texte` | `foreground` | `#ffffff` | couleur du texte |
96
+ | `accent` | — | `#00ff99` | pastille, surtitre, point final, motif |
97
+ | `motif` | — | `particules` | `particules`, `grille` ou `aucun` |
98
+ | `graine` | `seed` | `0` | change la disposition du motif sans rien toucher d'autre |
99
+ | `police` | `font` | JetBrains Mono | nom de famille, gabarit de chemin contenant `%s`, ou `{regular:, bold:}` |
100
+ | `formats` | — | `[16x9, 4x3, 1x1]` | ratios connus ou tailles explicites en pixels |
101
+ | `nom` | `name` | `article` | base du nom de fichier |
102
+ | `sortie` | `out` | `.` | dossier de sortie, relatif au fichier YAML |
103
+ | `point` | `dot` | `true` | le point d'accent après le titre |
104
+ | `lignes-max` | `max_lines` | `4` | au-delà, le titre rapetisse au lieu de déborder |
105
+
106
+ Les couleurs s'écrivent `#rgb`, `#rrggbb` ou `[28, 28, 34]`. Une clé inconnue
107
+ est une erreur : une faute de frappe dans un nom de clé ne se voit sinon qu'une
108
+ fois la carte terminée.
109
+
110
+ Ratios connus : `16x9`, `4x3`, `3x2`, `2x1`, `1x1`, `4x5`, `9x16`. Pour tout
111
+ autre besoin, écrivez la taille en pixels — `1240x700`.
112
+
113
+ Voir [`examples/complet.yml`](examples/complet.yml) pour un fichier avec toutes
114
+ les clés.
115
+
116
+ ## En ligne de commande
117
+
118
+ ```console
119
+ $ blog-image image.yml # formats du fichier
120
+ $ blog-image image.yml -o public/blog # ailleurs
121
+ $ blog-image image.yml -f 16x9 -f 1200x628 # autres formats
122
+ $ blog-image image.yml --dry-run # ce qui serait écrit, sans écrire
123
+ ```
124
+
125
+ `-n/--name` remplace la base du nom de fichier, `-q/--quiet` coupe la sortie.
126
+ Une erreur de description sort en code 2 avec un message en clair, pas une
127
+ trace.
128
+
129
+ ## Comme bibliothèque
130
+
131
+ ```python
132
+ from pathlib import Path
133
+ from blog_image import load_spec, render, resolve_fonts
134
+
135
+ spec = load_spec(Path("image.yml"))
136
+ fonts = resolve_fonts(spec.font)
137
+ for fmt in spec.formats:
138
+ render(spec, fonts, fmt, spec.path_for(fmt))
139
+ ```
140
+
141
+ ## Trois décisions qui expliquent le reste du code
142
+
143
+ **Tout est dessiné à 3× puis réduit en Lanczos.** C'est ce qui tient les
144
+ filets d'un pixel et le petit corps, pas l'anticrénelage du rastériseur, qui
145
+ est inégal à ces tailles. La réduction est suivie d'une quantification sur 256
146
+ couleurs : sur un aplat, elle ne se voit pas et divise le poids du fichier par
147
+ deux.
148
+
149
+ **Le corps du texte est calculé sur une unité, pas sur la largeur.** Une carte
150
+ 1:1 n'est pas plus large qu'une 4:3 mais elle a bien plus de place ; dimensionner
151
+ sur la largeur seule la laisse creuse. L'unité corrige la largeur par l'écart
152
+ entre le ratio demandé et le 16:9 :
153
+
154
+ ```python
155
+ unit = width * (1 + max(0.0, height / width - 0.5625) * 0.34)
156
+ ```
157
+
158
+ **Le titre est coupé sur la largeur mesurée de la police**, pas sur un nombre
159
+ de caractères, et le corps diminue tant que le compte de lignes dépasse
160
+ `lignes-max`. C'est ce qui évite d'écrire une coupe différente par format.
161
+
162
+ Le motif, lui, est dimensionné sur la même unité et non sur la bande qu'il
163
+ occupe : en 16:9 cette bande fait quelques centaines de pixels, en 1:1 cinq
164
+ fois plus, et un décor calé sur la bande disparaît dans le premier cas et
165
+ écrase le second. Seul le nombre de marques dépend de la place disponible.
166
+
167
+ ## Tests
168
+
169
+ ```console
170
+ $ uv run pytest
171
+ ```
172
+
173
+ Le rendu est reproductible : les mêmes descriptions donnent les mêmes fichiers,
174
+ octet pour octet, d'une version de Pillow à l'autre. `uv.lock` est versionné
175
+ pour que ce soit vrai d'une machine à l'autre aussi.
176
+
177
+ ## Journal des changements
178
+
179
+ Voir [CHANGELOG.md](CHANGELOG.md).
180
+
181
+ ## Contact
182
+
183
+ Une question d'usage, un titre qui se coupe mal, une idée de format : le
184
+ [Discord](https://discord.gg/FS2xqJ4QT6) est le chemin le plus court. Les questions posées dans les
185
+ [Discussions](https://github.com/foverseas/blog-image/discussions/categories/q-a) y gagnent une réponse qui
186
+ reste trouvable, et les bugs vont dans les [issues](https://github.com/foverseas/blog-image/issues).
187
+
188
+ ## Licence
189
+
190
+ MIT.
@@ -0,0 +1,165 @@
1
+ # blog-image
2
+
3
+ [![CI](https://github.com/foverseas/blog-image/actions/workflows/ci.yml/badge.svg)](https://github.com/foverseas/blog-image/actions/workflows/ci.yml)
4
+
5
+ Génère les images de couverture d'un article dans plusieurs formats, à partir
6
+ d'un fichier YAML de trois lignes.
7
+
8
+ ```yaml
9
+ # image.yml
10
+ titre: "Combien coûte une application mobile"
11
+ accent: "#00ff99"
12
+ fond: "#1c1c22"
13
+ ```
14
+
15
+ ```console
16
+ $ blog-image image.yml
17
+ -> article-16x9.png 1600x900
18
+ -> article-4x3.png 1600x1200
19
+ -> article-1x1.png 1200x1200
20
+ ```
21
+
22
+ ![Exemple de carte 16:9](https://raw.githubusercontent.com/foverseas/blog-image/v0.1.0/docs/exemple-16x9.png)
23
+
24
+ Trois formats parce qu'un article en a besoin de trois : le 16:9 pour Open
25
+ Graph et l'en-tête du billet, le 4:3 pour l'emplacement Discover, le 1:1 pour
26
+ les recadrages sociaux. Les générer séparément, c'est trois fichiers qui
27
+ divergent au premier changement de titre.
28
+
29
+ ## Installation
30
+
31
+ Avec [uv](https://docs.astral.sh/uv/), sans rien installer de permanent :
32
+
33
+ ```console
34
+ $ uvx --from blog-image blog-image image.yml
35
+ ```
36
+
37
+ Pour l'avoir sous la main partout :
38
+
39
+ ```console
40
+ $ uv tool install blog-image # ou : pipx install blog-image
41
+ ```
42
+
43
+ Et en développement, depuis les sources :
44
+
45
+ ```console
46
+ $ git clone https://github.com/foverseas/blog-image
47
+ $ cd blog-image
48
+ $ uv sync --extra dev # crée .venv d'après uv.lock
49
+ $ uv run blog-image examples/image.yml
50
+ ```
51
+
52
+ `pip install blog-image` fonctionne aussi : c'est un paquet ordinaire, uv n'est
53
+ pas une dépendance.
54
+
55
+ Il faut Python 3.10, Pillow et PyYAML. La police par défaut est JetBrains
56
+ Mono ; si elle n'est pas installée, le programme retombe sur IBM Plex Mono,
57
+ DejaVu puis Arial, et dit où il a cherché s'il ne trouve rien.
58
+
59
+ ## Le fichier de description
60
+
61
+ Seul `titre` est obligatoire. Les clés s'écrivent en français ou en anglais.
62
+
63
+ | Clé | Alias | Défaut | Rôle |
64
+ | --- | --- | --- | --- |
65
+ | `titre` | `title` | — | le titre, coupé automatiquement sur la largeur réelle de la police |
66
+ | `sous-titre` | `subtitle` | vide | une ligne sous le titre ; une liste devient autant de lignes |
67
+ | `chapeau` | `eyebrow` | vide | surtitre en capitales espacées, dans la couleur d'accent |
68
+ | `pied` | `footer` | vide | le pied de carte, précédé d'une pastille |
69
+ | `fond` | `background` | `#1c1c22` | fond de la carte |
70
+ | `texte` | `foreground` | `#ffffff` | couleur du texte |
71
+ | `accent` | — | `#00ff99` | pastille, surtitre, point final, motif |
72
+ | `motif` | — | `particules` | `particules`, `grille` ou `aucun` |
73
+ | `graine` | `seed` | `0` | change la disposition du motif sans rien toucher d'autre |
74
+ | `police` | `font` | JetBrains Mono | nom de famille, gabarit de chemin contenant `%s`, ou `{regular:, bold:}` |
75
+ | `formats` | — | `[16x9, 4x3, 1x1]` | ratios connus ou tailles explicites en pixels |
76
+ | `nom` | `name` | `article` | base du nom de fichier |
77
+ | `sortie` | `out` | `.` | dossier de sortie, relatif au fichier YAML |
78
+ | `point` | `dot` | `true` | le point d'accent après le titre |
79
+ | `lignes-max` | `max_lines` | `4` | au-delà, le titre rapetisse au lieu de déborder |
80
+
81
+ Les couleurs s'écrivent `#rgb`, `#rrggbb` ou `[28, 28, 34]`. Une clé inconnue
82
+ est une erreur : une faute de frappe dans un nom de clé ne se voit sinon qu'une
83
+ fois la carte terminée.
84
+
85
+ Ratios connus : `16x9`, `4x3`, `3x2`, `2x1`, `1x1`, `4x5`, `9x16`. Pour tout
86
+ autre besoin, écrivez la taille en pixels — `1240x700`.
87
+
88
+ Voir [`examples/complet.yml`](examples/complet.yml) pour un fichier avec toutes
89
+ les clés.
90
+
91
+ ## En ligne de commande
92
+
93
+ ```console
94
+ $ blog-image image.yml # formats du fichier
95
+ $ blog-image image.yml -o public/blog # ailleurs
96
+ $ blog-image image.yml -f 16x9 -f 1200x628 # autres formats
97
+ $ blog-image image.yml --dry-run # ce qui serait écrit, sans écrire
98
+ ```
99
+
100
+ `-n/--name` remplace la base du nom de fichier, `-q/--quiet` coupe la sortie.
101
+ Une erreur de description sort en code 2 avec un message en clair, pas une
102
+ trace.
103
+
104
+ ## Comme bibliothèque
105
+
106
+ ```python
107
+ from pathlib import Path
108
+ from blog_image import load_spec, render, resolve_fonts
109
+
110
+ spec = load_spec(Path("image.yml"))
111
+ fonts = resolve_fonts(spec.font)
112
+ for fmt in spec.formats:
113
+ render(spec, fonts, fmt, spec.path_for(fmt))
114
+ ```
115
+
116
+ ## Trois décisions qui expliquent le reste du code
117
+
118
+ **Tout est dessiné à 3× puis réduit en Lanczos.** C'est ce qui tient les
119
+ filets d'un pixel et le petit corps, pas l'anticrénelage du rastériseur, qui
120
+ est inégal à ces tailles. La réduction est suivie d'une quantification sur 256
121
+ couleurs : sur un aplat, elle ne se voit pas et divise le poids du fichier par
122
+ deux.
123
+
124
+ **Le corps du texte est calculé sur une unité, pas sur la largeur.** Une carte
125
+ 1:1 n'est pas plus large qu'une 4:3 mais elle a bien plus de place ; dimensionner
126
+ sur la largeur seule la laisse creuse. L'unité corrige la largeur par l'écart
127
+ entre le ratio demandé et le 16:9 :
128
+
129
+ ```python
130
+ unit = width * (1 + max(0.0, height / width - 0.5625) * 0.34)
131
+ ```
132
+
133
+ **Le titre est coupé sur la largeur mesurée de la police**, pas sur un nombre
134
+ de caractères, et le corps diminue tant que le compte de lignes dépasse
135
+ `lignes-max`. C'est ce qui évite d'écrire une coupe différente par format.
136
+
137
+ Le motif, lui, est dimensionné sur la même unité et non sur la bande qu'il
138
+ occupe : en 16:9 cette bande fait quelques centaines de pixels, en 1:1 cinq
139
+ fois plus, et un décor calé sur la bande disparaît dans le premier cas et
140
+ écrase le second. Seul le nombre de marques dépend de la place disponible.
141
+
142
+ ## Tests
143
+
144
+ ```console
145
+ $ uv run pytest
146
+ ```
147
+
148
+ Le rendu est reproductible : les mêmes descriptions donnent les mêmes fichiers,
149
+ octet pour octet, d'une version de Pillow à l'autre. `uv.lock` est versionné
150
+ pour que ce soit vrai d'une machine à l'autre aussi.
151
+
152
+ ## Journal des changements
153
+
154
+ Voir [CHANGELOG.md](CHANGELOG.md).
155
+
156
+ ## Contact
157
+
158
+ Une question d'usage, un titre qui se coupe mal, une idée de format : le
159
+ [Discord](https://discord.gg/FS2xqJ4QT6) est le chemin le plus court. Les questions posées dans les
160
+ [Discussions](https://github.com/foverseas/blog-image/discussions/categories/q-a) y gagnent une réponse qui
161
+ reste trouvable, et les bugs vont dans les [issues](https://github.com/foverseas/blog-image/issues).
162
+
163
+ ## Licence
164
+
165
+ MIT.
@@ -0,0 +1,8 @@
1
+ """Generate article cover images in several aspect ratios from a short YAML file."""
2
+
3
+ from .config import Format, Spec, load_spec
4
+ from .fonts import FontSet, resolve_fonts
5
+ from .render import render
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = ["FontSet", "Format", "Spec", "load_spec", "render", "resolve_fonts"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,61 @@
1
+ """Command line entry point: `blog-image image.yml`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .config import SpecError, load_spec, parse_format, slugify
11
+ from .fonts import FontError, resolve_fonts
12
+ from .render import render
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="blog-image",
18
+ description="Generate article cover images in several aspect ratios from a short YAML file.",
19
+ )
20
+ parser.add_argument("spec", type=Path, help="YAML file describing the card")
21
+ parser.add_argument("-o", "--out", type=Path, help="output directory (overrides `sortie`)")
22
+ parser.add_argument(
23
+ "-f",
24
+ "--format",
25
+ action="append",
26
+ dest="formats",
27
+ metavar="RATIO",
28
+ help="format to render, repeatable; overrides `formats` (e.g. 16x9, 1x1, 1600x900)",
29
+ )
30
+ parser.add_argument("-n", "--name", help="base name for the files (overrides `nom`)")
31
+ parser.add_argument("--dry-run", action="store_true", help="list what would be written and stop")
32
+ parser.add_argument("-q", "--quiet", action="store_true", help="print nothing on success")
33
+ parser.add_argument("--version", action="version", version=f"blog-image {__version__}")
34
+ return parser
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> int:
38
+ args = build_parser().parse_args(argv)
39
+
40
+ try:
41
+ spec = load_spec(args.spec)
42
+ if args.formats:
43
+ spec.formats = tuple(parse_format(token) for token in args.formats)
44
+ if args.out is not None:
45
+ spec.out = args.out
46
+ if args.name:
47
+ spec.name = slugify(args.name)
48
+ fonts = None if args.dry_run else resolve_fonts(spec.font)
49
+ except (SpecError, FontError) as error:
50
+ print(f"blog-image: {error}", file=sys.stderr)
51
+ return 2
52
+
53
+ for fmt in spec.formats:
54
+ path = spec.path_for(fmt)
55
+ if not args.dry_run:
56
+ render(spec, fonts, fmt, path)
57
+ if not args.quiet:
58
+ prefix = "would write" if args.dry_run else "->"
59
+ print(f" {prefix} {path} {fmt.width}x{fmt.height}")
60
+
61
+ return 0