1
etude_lego_jurassic_world/lib/plots/minifig_character_collages.py

124 lines
4.8 KiB
Python

"""Génère des frises horizontales de minifigs par personnage."""
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Set
from PIL import Image, ImageDraw, ImageFont
from lib.filesystem import ensure_parent_dir
from lib.rebrickable.resources import sanitize_name
def resize_to_height(image: Image.Image, target_height: int) -> Image.Image:
"""Redimensionne une image en conservant le ratio selon une hauteur cible."""
width, height = image.size
ratio = target_height / height
new_width = int(width * ratio)
return image.resize((new_width, target_height), Image.Resampling.LANCZOS)
def render_label(width: int, height: int, text: str, font: ImageFont.ImageFont) -> Image.Image:
"""Construit l'étiquette textuelle centrée sous une minifig."""
label = Image.new("RGB", (width, height), (255, 255, 255))
drawer = ImageDraw.Draw(label)
bbox = drawer.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (width - text_width) // 2
y = (height - text_height) // 2
drawer.text((x, y), text, fill=(0, 0, 0), font=font)
return label
def build_placeholder(image_height: int) -> Image.Image:
"""Crée un rectangle neutre pour signaler une image manquante."""
placeholder = Image.new("RGBA", (image_height, image_height), (220, 220, 220, 255))
drawer = ImageDraw.Draw(placeholder)
drawer.rectangle([(0, 0), (image_height - 1, image_height - 1)], outline=(150, 150, 150), width=2)
drawer.line([(0, 0), (image_height - 1, image_height - 1)], fill=(200, 80, 80), width=3)
drawer.line([(image_height - 1, 0), (0, image_height - 1)], fill=(200, 80, 80), width=3)
return placeholder
def build_cell(image: Image.Image, label: str, image_height: int, label_height: int, font: ImageFont.ImageFont) -> Image.Image:
"""Assemble une minifig redimensionnée et son étiquette associée."""
resized = resize_to_height(image, image_height)
label_img = render_label(resized.width, label_height, label, font).convert("RGBA")
cell = Image.new("RGBA", (resized.width, resized.height + label_height), (255, 255, 255, 255))
cell.paste(resized, (0, 0))
cell.paste(label_img, (0, resized.height))
return cell
def build_character_collage(
character: str,
entries: Sequence[dict],
resources_dir: Path,
destination_dir: Path,
font: ImageFont.ImageFont,
missing_paths: Set[str] | None = None,
image_filename: str = "minifig.jpg",
label_field: str = "fig_num",
image_height: int = 260,
label_height: int = 44,
spacing: int = 28,
) -> Path:
"""Construit la frise d'un personnage et la sauvegarde dans le répertoire cible."""
sanitized = sanitize_name(character)
missing = missing_paths or set()
cells: List[Image.Image] = []
for row in entries:
image_path = resources_dir / row["set_id"] / sanitized / image_filename
owned = "*" if row.get("in_collection", "").lower() == "true" else ""
label_value = row[label_field]
label = f"{row['year']} - {row['set_num']}{owned} ({label_value})"
if str(image_path) in missing:
image = build_placeholder(image_height)
else:
image = Image.open(image_path).convert("RGBA")
cells.append(build_cell(image, label, image_height, label_height, font))
total_width = sum(cell.width for cell in cells) + spacing * (len(cells) - 1)
total_height = max(cell.height for cell in cells)
canvas = Image.new("RGB", (total_width, total_height), (255, 255, 255))
x = 0
for cell in cells:
canvas.paste(cell.convert("RGB"), (x, 0))
x += cell.width + spacing
destination_path = destination_dir / f"{sanitized}.png"
ensure_parent_dir(destination_path)
canvas.save(destination_path)
return destination_path
def build_character_collages(
grouped_entries: Dict[str, Sequence[dict]],
resources_dir: Path,
destination_dir: Path,
missing_paths: Set[str] | None = None,
image_filename: str = "minifig.jpg",
label_field: str = "fig_num",
image_height: int = 260,
label_height: int = 44,
spacing: int = 28,
) -> List[Path]:
"""Construit les frises pour chaque personnage."""
font = ImageFont.load_default()
generated: List[Path] = []
for character, entries in grouped_entries.items():
generated.append(
build_character_collage(
character,
entries,
resources_dir,
destination_dir,
font,
missing_paths=missing_paths,
image_filename=image_filename,
label_field=label_field,
image_height=image_height,
label_height=label_height,
spacing=spacing,
)
)
return generated