# 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=`) - 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