import initial
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Design : affichage de l'image originale sur la page solution
|
||||
|
||||
**Date :** 2026-05-20
|
||||
|
||||
## Contexte
|
||||
|
||||
La page solution affiche actuellement la grille remplie (cellules noires). L'utilisateur souhaite voir également l'image d'origine dont la grille est issue, pour faciliter la vérification et enrichir le rendu visuel.
|
||||
|
||||
## Mise en page choisie
|
||||
|
||||
Page portrait A4. La page solution est divisée en **deux colonnes égales** :
|
||||
|
||||
- **Colonne gauche** : grille solution (indices + cellules remplies) — identique à aujourd'hui mais réduite pour tenir dans la moitié de la largeur
|
||||
- **Colonne droite** : image originale redimensionnée à la même hauteur et largeur que la grille, centrée verticalement
|
||||
|
||||
Espacement entre les deux colonnes : gouttière fixe de 10 mm.
|
||||
|
||||
Si `image_bytes` est `None`, la page solution se comporte comme avant (grille seule, pleine largeur).
|
||||
|
||||
## Changements requis
|
||||
|
||||
### 1. `src/logimage/domain/entities/puzzle.py`
|
||||
|
||||
Ajouter un champ `image_bytes: bytes | None = None` à `NonogramPuzzle` :
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class NonogramPuzzle:
|
||||
grid: Grid
|
||||
row_clues: tuple[Clue, ...]
|
||||
col_clues: tuple[Clue, ...]
|
||||
title: str
|
||||
image_bytes: bytes | None = None
|
||||
```
|
||||
|
||||
Étendre `from_grid()` pour accepter et transmettre ce paramètre :
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def from_grid(cls, grid: Grid, title: str, image_bytes: bytes | None = None) -> "NonogramPuzzle":
|
||||
...
|
||||
return cls(grid=grid, row_clues=row_clues, col_clues=col_clues, title=title, image_bytes=image_bytes)
|
||||
```
|
||||
|
||||
### 2. `src/logimage/application/use_cases/generate_puzzles.py`
|
||||
|
||||
Passer `image_data.content` lors de la construction du puzzle :
|
||||
|
||||
```python
|
||||
puzzle = NonogramPuzzle.from_grid(grid, image_data.title, image_data.content)
|
||||
```
|
||||
|
||||
### 3. `src/logimage/infrastructure/pdf/reportlab_exporter.py`
|
||||
|
||||
Modifier `_draw_page()` pour la page solution (`filled=True`) :
|
||||
|
||||
- Si `puzzle.image_bytes` est `None` : comportement actuel inchangé.
|
||||
- Si `puzzle.image_bytes` est présent : calculer la largeur disponible en divisant en deux colonnes (`(avail_w - gutter) / 2`). Réduire la grille à cette demi-largeur. Rendre l'image dans la colonne droite via `reportlab.lib.utils.ImageReader` avec `canvas.drawImage()`, aux mêmes dimensions que la grille (même x, y, width, height).
|
||||
|
||||
## Ce qui ne change pas
|
||||
|
||||
- La page puzzle (non solution) reste inchangée
|
||||
- Le port `PdfExporter` reste inchangé (signature de `export()` inchangée)
|
||||
- Le port `ImageSource` reste inchangé
|
||||
- Les tests existants restent valides (le champ `image_bytes` est optionnel)
|
||||
|
||||
## Tests
|
||||
|
||||
- `NonogramPuzzle.from_grid()` avec et sans `image_bytes`
|
||||
- `ReportLabPdfExporter` : vérifier que le PDF généré avec `with_solution=True` et des bytes image est plus grand (taille fichier) que sans image
|
||||
- Vérifier que `with_solution=True` sans `image_bytes` se comporte comme avant (rétrocompatibilité)
|
||||
@@ -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
|
||||
@@ -0,0 +1,45 @@
|
||||
# Design : intégration Pexels et aiguillage multi-provider
|
||||
|
||||
**Date :** 2026-05-20
|
||||
|
||||
## Contexte
|
||||
|
||||
Le projet dispose déjà d'un port `ImageSource` (ABC) et d'une implémentation `UnsplashImageSource`. La clé API Unsplash n'est pas encore disponible ; une clé Pexels est disponible maintenant. L'objectif est d'ajouter le support Pexels et de router automatiquement vers le bon provider selon les variables d'environnement.
|
||||
|
||||
## Périmètre
|
||||
|
||||
- Ajouter `PexelsImageSource` dans l'infrastructure
|
||||
- Modifier `main.py` pour router selon les clés d'API présentes
|
||||
- Pas de changement au domain, au port, ni au use case
|
||||
|
||||
## Nouveau fichier : `PexelsImageSource`
|
||||
|
||||
**Chemin :** `src/logimage/infrastructure/image/pexels_source.py`
|
||||
|
||||
Implémente `ImageSource`. Deux endpoints selon la présence d'un thème :
|
||||
- Avec thème : `GET https://api.pexels.com/v1/search?query=<theme>&per_page=1`
|
||||
- Sans thème : `GET https://api.pexels.com/v1/curated?per_page=1`
|
||||
|
||||
Authentification : header `Authorization: <api_key>` (pas de préfixe).
|
||||
|
||||
Extraction des données :
|
||||
- URL image : `photo["src"]["large"]`
|
||||
- Titre : `photo["alt"]` ou `theme` ou `"logimage"` en fallback
|
||||
|
||||
## Modification : routing dans `main.py`
|
||||
|
||||
Remplacer la logique actuelle (Unsplash uniquement) par :
|
||||
|
||||
1. Lire `PEXELS_API_KEY` et `UNSPLASH_ACCESS_KEY` depuis l'environnement
|
||||
2. Si `PEXELS_API_KEY` est présente → instancier `PexelsImageSource`
|
||||
3. Sinon si `UNSPLASH_ACCESS_KEY` est présente → instancier `UnsplashImageSource`
|
||||
4. Sinon → afficher une erreur claire et quitter :
|
||||
`"Error: set PEXELS_API_KEY or UNSPLASH_ACCESS_KEY in .env"`
|
||||
|
||||
Pexels est prioritaire car c'est la clé disponible aujourd'hui.
|
||||
|
||||
## Ce qui ne change pas
|
||||
|
||||
- Le port `ImageSource` reste inchangé
|
||||
- Le use case `GeneratePuzzlesUseCase` reste inchangé
|
||||
- `UnsplashImageSource` reste inchangé (prêt pour quand la clé sera disponible)
|
||||
@@ -0,0 +1,54 @@
|
||||
# Amélioration du pipeline de conversion image → nonogram
|
||||
|
||||
## Objectif
|
||||
|
||||
Produire des grilles de nonogram à la fois jouables (≤ 6 blocs par ligne/colonne) et reconnaissables (ressemblance avec l'image source), à partir de photos Pexels/Unsplash ou d'images locales.
|
||||
|
||||
## Problème actuel
|
||||
|
||||
Le pipeline actuel (edge detection Canny + blend + Otsu) génère trop de pixels isolés sur les photos, produisant des clues illisibles (ex: 15 blocs sur une ligne de 32 cellules) et une grille méconnaissable.
|
||||
|
||||
## Pipeline cible
|
||||
|
||||
```
|
||||
Image bytes
|
||||
→ Grayscale + resize (inchangé)
|
||||
→ Bilateral filter (d=9, sigmaColor=75, sigmaSpace=75)
|
||||
→ Posterize 2 niveaux (seuillage médian sur histogramme)
|
||||
→ Binarisation Otsu
|
||||
→ Fermeture morphologique kernel 3×3 (combler micro-trous)
|
||||
→ Suppression composantes connexes < min_component_size (défaut: 2px)
|
||||
→ [Boucle] Vérification complexité clues
|
||||
si max_blocks_per_line > max_clue_blocks :
|
||||
fermeture morphologique kernel += 1
|
||||
max 3 itérations
|
||||
→ Grid
|
||||
```
|
||||
|
||||
## Paramètres configurables
|
||||
|
||||
| Paramètre | Défaut | Description |
|
||||
|---|---|---|
|
||||
| `max_clue_blocks` | 6 | Max blocs acceptables par ligne/colonne |
|
||||
| `min_component_size` | 2 | Taille min composante connexe (px) |
|
||||
| `max_cleanup_iterations` | 3 | Nb max d'itérations de nettoyage supplémentaire |
|
||||
|
||||
## Nouvelle source locale
|
||||
|
||||
`LocalFileImageSource` — implémente `ImageSource`, lit un fichier depuis le disque.
|
||||
|
||||
CLI : nouvelle option `--local-image <path>` mutuellement exclusive avec `--theme`.
|
||||
|
||||
## Tests
|
||||
|
||||
- **Unitaires** : bilateral filter, cleanup morpho, boucle complexité sur grilles synthétiques
|
||||
- **Intégration** : image simple → vérifie max blocs/ligne ≤ 6
|
||||
- **Fixture** : `tests/fixtures/simple_icon.png` généré programmatiquement (forme simple)
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
- `src/logimage/infrastructure/image/pillow_converter.py` — pipeline complet
|
||||
- `src/logimage/infrastructure/image/local_file_source.py` — nouvelle source
|
||||
- `src/logimage/cli/main.py` — option `--local-image`
|
||||
- `tests/infrastructure/test_pillow_converter.py` — nouveaux tests
|
||||
- `tests/fixtures/simple_icon.png` — image de test
|
||||
Reference in New Issue
Block a user