blog-image
Génère les images de couverture d'un article dans plusieurs formats, à partir d'un fichier YAML de trois lignes.
# image.yml
titre: "Combien coûte une application mobile"
accent: "#00ff99"
fond: "#1c1c22"
$ blog-image image.yml
-> article-16x9.png 1600x900
-> article-4x3.png 1600x1200
-> article-1x1.png 1200x1200
Trois formats parce qu'un article en a besoin de trois : le 16:9 pour Open Graph et l'en-tête du billet, le 4:3 pour l'emplacement Discover, le 1:1 pour les recadrages sociaux. Les générer séparément, c'est trois fichiers qui divergent au premier changement de titre.
Installation
Avec uv, sans rien installer de permanent :
$ uvx --from blog-image blog-image image.yml
Pour l'avoir sous la main partout :
$ uv tool install blog-image # ou : pipx install blog-image
Et en développement, depuis les sources :
$ git clone https://github.com/foverseas/blog-image
$ cd blog-image
$ uv sync --extra dev # crée .venv d'après uv.lock
$ uv run blog-image examples/image.yml
pip install blog-image fonctionne aussi : c'est un paquet ordinaire, uv n'est
pas une dépendance.
Il faut Python 3.10, Pillow et PyYAML. La police par défaut est JetBrains Mono ; si elle n'est pas installée, le programme retombe sur IBM Plex Mono, DejaVu puis Arial, et dit où il a cherché s'il ne trouve rien.
Le fichier de description
Seul titre est obligatoire. Les clés s'écrivent en français ou en anglais.
| Clé | Alias | Défaut | Rôle |
|---|---|---|---|
titre |
title |
— | le titre, coupé automatiquement sur la largeur réelle de la police |
sous-titre |
subtitle |
vide | une ligne sous le titre ; une liste devient autant de lignes |
chapeau |
eyebrow |
vide | surtitre en capitales espacées, dans la couleur d'accent |
pied |
footer |
vide | le pied de carte, précédé d'une pastille |
fond |
background |
#1c1c22 |
fond de la carte |
texte |
foreground |
#ffffff |
couleur du texte |
accent |
— | #00ff99 |
pastille, surtitre, point final, motif |
motif |
— | particules |
particules, grille ou aucun |
graine |
seed |
0 |
change la disposition du motif sans rien toucher d'autre |
police |
font |
JetBrains Mono | nom de famille, gabarit de chemin contenant %s, ou {regular:, bold:} |
formats |
— | [16x9, 4x3, 1x1] |
ratios connus ou tailles explicites en pixels |
nom |
name |
article |
base du nom de fichier |
sortie |
out |
. |
dossier de sortie, relatif au fichier YAML |
point |
dot |
true |
le point d'accent après le titre |
lignes-max |
max_lines |
4 |
au-delà, le titre rapetisse au lieu de déborder |
Les couleurs s'écrivent #rgb, #rrggbb ou [28, 28, 34]. Une clé inconnue
est une erreur : une faute de frappe dans un nom de clé ne se voit sinon qu'une
fois la carte terminée.
Ratios connus : 16x9, 4x3, 3x2, 2x1, 1x1, 4x5, 9x16. Pour tout
autre besoin, écrivez la taille en pixels — 1240x700.
Voir examples/complet.yml pour un fichier avec toutes
les clés.
En ligne de commande
$ blog-image image.yml # formats du fichier
$ blog-image image.yml -o public/blog # ailleurs
$ blog-image image.yml -f 16x9 -f 1200x628 # autres formats
$ blog-image image.yml --dry-run # ce qui serait écrit, sans écrire
-n/--name remplace la base du nom de fichier, -q/--quiet coupe la sortie.
Une erreur de description sort en code 2 avec un message en clair, pas une
trace.
Comme bibliothèque
from pathlib import Path
from blog_image import load_spec, render, resolve_fonts
spec = load_spec(Path("image.yml"))
fonts = resolve_fonts(spec.font)
for fmt in spec.formats:
render(spec, fonts, fmt, spec.path_for(fmt))
Trois décisions qui expliquent le reste du code
Tout est dessiné à 3× puis réduit en Lanczos. C'est ce qui tient les filets d'un pixel et le petit corps, pas l'anticrénelage du rastériseur, qui est inégal à ces tailles. La réduction est suivie d'une quantification sur 256 couleurs : sur un aplat, elle ne se voit pas et divise le poids du fichier par deux.
Le corps du texte est calculé sur une unité, pas sur la largeur. Une carte 1:1 n'est pas plus large qu'une 4:3 mais elle a bien plus de place ; dimensionner sur la largeur seule la laisse creuse. L'unité corrige la largeur par l'écart entre le ratio demandé et le 16:9 :
unit = width * (1 + max(0.0, height / width - 0.5625) * 0.34)
Le titre est coupé sur la largeur mesurée de la police, pas sur un nombre
de caractères, et le corps diminue tant que le compte de lignes dépasse
lignes-max. C'est ce qui évite d'écrire une coupe différente par format.
Le motif, lui, est dimensionné sur la même unité et non sur la bande qu'il occupe : en 16:9 cette bande fait quelques centaines de pixels, en 1:1 cinq fois plus, et un décor calé sur la bande disparaît dans le premier cas et écrase le second. Seul le nombre de marques dépend de la place disponible.
Tests
$ uv run pytest
Le rendu est reproductible : les mêmes descriptions donnent les mêmes fichiers,
octet pour octet, d'une version de Pillow à l'autre. uv.lock est versionné
pour que ce soit vrai d'une machine à l'autre aussi.
Journal des changements
Voir CHANGELOG.md.
Contact
Une question d'usage, un titre qui se coupe mal, une idée de format : le Discord est le chemin le plus court. Les questions posées dans les Discussions y gagnent une réponse qui reste trouvable, et les bugs vont dans les issues.
Licence
MIT.
Metadata
Release files for blog-image 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| blog_image-0.2.0.tar.gz | 104.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| blog_image-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 128.5 kB
Release files / blog_image-0.2.0.tar.gz
| Download URL | blog_image-0.2.0.tar.gz |
|---|---|
| Size | 104.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b6a315fed7b08726bd65a9ceea72456c39f65e43822a36d277cbe1c5f43e0f17
|
|
BLAKE2b-256 checksum How to use checksums |
fdefdf7a524f9f330c09fd81abca95d563c8bdaab62260379cabaa8908d24610
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / blog_image-0.2.0-py3-none-any.whl
| Download URL | blog_image-0.2.0-py3-none-any.whl |
|---|---|
| Size | 24.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2db23f3faa84a7b689787c2c330f6494a8ae938fff52efcb244cca283b6d94a3
|
|
BLAKE2b-256 checksum How to use checksums |
d7ba10972f2e516e0cb0159ca9eeab8f4feca967ff803207fa6479682d7d796e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for blog-image 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for blog_image-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.