Files
logimage-generator/docs/superpowers/specs/2026-05-20-logimage-generator-design.md
T
Vincent Bourdon 5a03f8a38d import initial
2026-06-10 10:21:18 +02:00

281 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
# 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 `ValueError` si 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 solution
- `row_clues: tuple[Clue]` — calculées à la construction
- `col_clues: tuple[Clue]` — calculées à la construction
- `title: str` — thème ou nom de l'image source
- Fabriqué via `NonogramPuzzle.from_grid(grid, title)` qui calcule les clues
### Ports (interfaces)
```python
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 :
1. Valider les paramètres (taille, count)
2. Pour chaque grille à générer :
a. `image_source.fetch(theme)``bytes`
b. `image_converter.to_grid(bytes, width, height)``Grid`
c. `NonogramPuzzle.from_grid(grid, title)``NonogramPuzzle`
3. `pdf_exporter.export(puzzles, path, with_solution)`
---
## 5. Infrastructure
### `UnsplashImageSource`
- Utilise `httpx` pour 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 × height` pixels
- 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
```bash
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)
```toml
[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