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

7.6 KiB
Raw Blame History

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 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)

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

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