import initial
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user