7.6 KiB
7.6 KiB
Logimage Generator — Design Spec
Date: 2026-05-20
Résumé
Outil CLI Python qui génère des nonogrammes (logimages) en PDF prêts à imprimer. Le programme récupère automatiquement des images depuis Unsplash, les convertit en grilles noir/blanc optimisées pour la jouabilité, calcule les indices lignes/colonnes, et produit un PDF A4 avec une grille centrée par page.
1. Comportement utilisateur
Invocation
# Mode minimal — 1 grille, thème aléatoire, difficulté medium
logimage
# Avec options
logimage \
--theme "animals" \
--difficulty medium \
--size 20x15 \
--count 5 \
--solution \
--output puzzles.pdf
Paramètres CLI
| Flag | Défaut | Description |
|---|---|---|
--theme TEXT |
aléatoire | Mot-clé pour la recherche d'images Unsplash |
--difficulty easy|medium|hard |
medium |
10×10 / 15×15 / 20×20 |
--size NxM |
— | Taille libre ; écrase --difficulty si fourni |
--count N |
1 |
Nombre de grilles dans le PDF |
--solution |
off | Ajoute une page solution après chaque grille |
--output PATH |
puzzles.pdf |
Chemin du fichier PDF de sortie |
Configuration
Clé API Unsplash via variable d'environnement ou fichier .env à la racine :
UNSPLASH_ACCESS_KEY=xxx
Format PDF
- Format A4 portrait, une grille par page
- Mise en page : titre en haut, grille centrée, thème en bas
- Grille vierge (cases à colorier), indices lignes à gauche, indices colonnes en haut
- Si
--solution: page solution insérée après chaque grille (grille pré-remplie)
2. Architecture (Clean Architecture)
La règle de dépendance est stricte : les couches internes ne connaissent jamais les couches externes.
domain ← application ← infrastructure ← cli
Structure des fichiers
src/logimage/
├── domain/
│ ├── entities/
│ │ └── puzzle.py # NonogramPuzzle
│ ├── value_objects/
│ │ ├── grid.py # Grid (immuable)
│ │ └── clue.py # Clue (immuable)
│ └── ports/
│ ├── image_source.py # ImageSource (ABC)
│ ├── image_converter.py # ImageConverter (ABC)
│ └── pdf_exporter.py # PdfExporter (ABC)
│
├── application/
│ └── use_cases/
│ └── generate_puzzles.py # GeneratePuzzlesUseCase
│
├── infrastructure/
│ ├── image/
│ │ ├── unsplash_source.py # UnsplashImageSource
│ │ └── pillow_converter.py # PillowImageConverter
│ └── pdf/
│ └── reportlab_exporter.py
│
└── cli/
└── main.py # Composition root + argparse
tests/
├── domain/
├── application/
├── infrastructure/ # @pytest.mark.integration
└── fakes/ # FakeImageSource, FakeImageConverter, FakePdfExporter
3. Modèle du domaine
Value objects
Grid
- Données :
tuple[tuple[bool]](immuable) - Contraintes : largeur et hauteur entre 5 et 50
- Lève
ValueErrorsi les contraintes ne sont pas respectées
Clue
- Séquence immuable d'entiers positifs représentant les blocs d'une ligne ou colonne
- Ligne vide →
Clue([]) - Exemple :
[T,T,F,T,F,F,T,T,T]→Clue([2, 1, 3])
Entité
NonogramPuzzle
grid: Grid— la solutionrow_clues: tuple[Clue]— calculées à la constructioncol_clues: tuple[Clue]— calculées à la constructiontitle: str— thème ou nom de l'image source- Fabriqué via
NonogramPuzzle.from_grid(grid, title)qui calcule les clues
Ports (interfaces)
class ImageSource(ABC):
def fetch(self, theme: str | None = None) -> bytes: ...
class ImageConverter(ABC):
def to_grid(self, image_bytes: bytes, width: int, height: int) -> Grid: ...
class PdfExporter(ABC):
def export(
self,
puzzles: list[NonogramPuzzle],
path: Path,
with_solution: bool = False,
) -> None: ...
4. Use case
GeneratePuzzlesUseCase
Dépendances injectées : ImageSource, ImageConverter, PdfExporter
Séquence d'exécution :
- Valider les paramètres (taille, count)
- Pour chaque grille à générer :
a.
image_source.fetch(theme)→bytesb.image_converter.to_grid(bytes, width, height)→Gridc.NonogramPuzzle.from_grid(grid, title)→NonogramPuzzle pdf_exporter.export(puzzles, path, with_solution)
5. Infrastructure
UnsplashImageSource
- Utilise
httpxpour appeler l'API Unsplash (/photos/random?query=<theme>) - Clé API lue depuis
UNSPLASH_ACCESS_KEY - Télécharge l'image en JPEG (taille raisonnable, ~800px)
PillowImageConverter
- Redimensionne l'image à
width × heightpixels - Conversion en niveaux de gris
- Détection de contours avec OpenCV (Canny) pour favoriser les formes reconnaissables
- Seuillage adaptatif pour maximiser la jouabilité (pas trop de cases noires, pas trop peu)
- Retourne un
Grid
ReportLabPdfExporter
- Format A4 portrait
- Grille rendue avec
reportlab(rectangles, texte pour les indices) - Indices colonnes en haut (empilés verticalement si plusieurs chiffres), indices lignes à gauche
- Marquage de groupes de 5 cases (bords plus épais) pour faciliter le comptage
- Page solution : même layout, cases noires pré-remplies
6. Stratégie de test (TDD)
Tests domain (purs, aucun mock)
test_clue_from_consecutive_cells()
test_clue_empty_row()
test_clue_single_cell()
test_puzzle_computes_row_and_col_clues()
test_grid_rejects_width_below_minimum()
test_grid_rejects_height_above_maximum()
test_grid_is_immutable()
Tests application (mocks des ports via fakes)
test_generate_single_puzzle_calls_fetch_once()
test_generate_count_5_calls_fetch_5_times()
test_solution_flag_forwarded_to_exporter()
test_invalid_size_raises_value_error()
test_size_from_difficulty_easy_is_10x10()
test_size_from_difficulty_hard_is_20x20()
Tests d'intégration (@pytest.mark.integration, réseau réel)
test_unsplash_returns_valid_jpeg_bytes()
test_pillow_converter_output_matches_requested_size()
test_reportlab_creates_nonempty_pdf_file()
Exécution
pytest tests/domain tests/application # rapide, CI toujours
pytest -m integration # ponctuel, nécessite UNSPLASH_ACCESS_KEY
pytest --cov=logimage --cov-report=term-missing
Couverture cible : ≥ 90% sur domain/ et application/.
7. Dépendances
| Rôle | Bibliothèque |
|---|---|
| Traitement image | Pillow |
| Détection contours | opencv-python-headless |
| Génération PDF | reportlab |
| HTTP | httpx |
| Variables d'env | python-dotenv |
| Tests | pytest, pytest-cov |
| Lint + format | ruff |
| Typage statique | mypy |
Python ≥ 3.11 requis.
pyproject.toml (structure)
[project]
name = "logimage-generator"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"Pillow", "opencv-python-headless", "reportlab",
"httpx", "python-dotenv",
]
[project.optional-dependencies]
dev = ["pytest", "pytest-cov", "ruff", "mypy"]
[project.scripts]
logimage = "logimage.cli.main:main"
[tool.pytest.ini_options]
markers = ["integration: tests requiring network and API keys"]
[tool.ruff]
line-length = 88
[tool.mypy]
strict = true
8. Ce qui est hors scope
- Interface graphique ou web
- Éditeur interactif de grille
- Validation de l'unicité de la solution (problème NP-difficile pour les grandes grilles)
- Cache local des images téléchargées (peut être ajouté plus tard)
- Support des nonogrammes en couleur