import initial

This commit is contained in:
Vincent Bourdon
2026-06-10 10:21:18 +02:00
commit 5a03f8a38d
59 changed files with 4777 additions and 0 deletions
@@ -0,0 +1,280 @@
# 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