You've already forked etude_lego_jurassic_world
Ajouter l’étape 35 : extraction et collage des autocollants
This commit is contained in:
70
lib/plots/sticker_sheets.py
Normal file
70
lib/plots/sticker_sheets.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Assemblage visuel des planches d'autocollants des sets filtrés."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from lib.filesystem import ensure_parent_dir
|
||||
from lib.rebrickable.stats import read_rows
|
||||
|
||||
|
||||
def load_sticker_parts(path: Path) -> List[dict]:
|
||||
"""Charge la liste des autocollants par set."""
|
||||
return read_rows(path)
|
||||
|
||||
|
||||
def plot_sticker_sheets(
|
||||
stickers_path: Path,
|
||||
destination_path: Path,
|
||||
resources_dir: Path = Path("figures/rebrickable"),
|
||||
columns: int = 6,
|
||||
) -> None:
|
||||
"""Assemble les images d'autocollants exclusifs en grille triée par année."""
|
||||
rows = load_sticker_parts(stickers_path)
|
||||
rows.sort(key=lambda r: (int(r["year"]), r["set_num"], r["part_num"]))
|
||||
selected: List[dict] = []
|
||||
images: List[Image.Image] = []
|
||||
for row in rows:
|
||||
image_path = resources_dir / row["set_id"] / "stickers" / f"{row['part_num']}.jpg"
|
||||
if not image_path.exists():
|
||||
continue
|
||||
img = Image.open(image_path).convert("RGBA")
|
||||
max_side = 260
|
||||
ratio = min(max_side / img.width, max_side / img.height, 1.0)
|
||||
if ratio < 1.0:
|
||||
img = img.resize((int(img.width * ratio), int(img.height * ratio)))
|
||||
images.append(img)
|
||||
selected.append(row)
|
||||
if not images:
|
||||
return
|
||||
|
||||
font = ImageFont.load_default()
|
||||
def measure(text: str) -> tuple[int, int]:
|
||||
bbox = ImageDraw.Draw(Image.new("RGB", (10, 10))).textbbox((0, 0), text, font=font)
|
||||
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
|
||||
labels = [f"{row['year']} • {row['set_id']} • {row['part_num']}" for row in selected]
|
||||
text_height = max(measure(label)[1] for label in labels)
|
||||
|
||||
columns = max(1, columns)
|
||||
rows_count = (len(images) + columns - 1) // columns
|
||||
cell_width = 280
|
||||
cell_height = 220 + text_height + 12
|
||||
width = columns * cell_width
|
||||
height = rows_count * cell_height
|
||||
canvas = Image.new("RGBA", (width, height), (255, 255, 255, 255))
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
for index, (img, label) in enumerate(zip(images, labels)):
|
||||
col = index % columns
|
||||
row_idx = index // columns
|
||||
x = col * cell_width + (cell_width - img.width) // 2
|
||||
y = row_idx * cell_height + 8
|
||||
canvas.paste(img, (x, y), img)
|
||||
text_width, _ = measure(label)
|
||||
text_x = col * cell_width + (cell_width - text_width) // 2
|
||||
text_y = y + img.height + 6
|
||||
draw.text((text_x, text_y), label, fill="#111111", font=font)
|
||||
|
||||
ensure_parent_dir(destination_path)
|
||||
canvas.convert("RGB").save(destination_path, "PNG")
|
||||
86
lib/rebrickable/sticker_parts.py
Normal file
86
lib/rebrickable/sticker_parts.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Sélection des planches d'autocollants pour les sets filtrés."""
|
||||
|
||||
import csv
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Tuple
|
||||
|
||||
from lib.filesystem import ensure_parent_dir
|
||||
from lib.rebrickable.stats import read_rows
|
||||
|
||||
|
||||
STICKER_CATEGORY_ID = "58"
|
||||
|
||||
|
||||
def load_parts_catalog(path: Path) -> Dict[str, dict]:
|
||||
"""Indexe les pièces par référence."""
|
||||
catalog: Dict[str, dict] = {}
|
||||
with path.open() as csv_file:
|
||||
reader = csv.DictReader(csv_file)
|
||||
for row in reader:
|
||||
catalog[row["part_num"]] = row
|
||||
return catalog
|
||||
|
||||
|
||||
def load_sets(path: Path) -> Dict[str, dict]:
|
||||
"""Indexe les sets enrichis par set_num."""
|
||||
lookup: Dict[str, dict] = {}
|
||||
for row in read_rows(path):
|
||||
lookup[row["set_num"]] = row
|
||||
return lookup
|
||||
|
||||
|
||||
def aggregate_stickers(
|
||||
rows: Iterable[dict],
|
||||
parts_catalog: Dict[str, dict],
|
||||
) -> Dict[Tuple[str, str], int]:
|
||||
"""Cumule les quantités d'autocollants par set et référence."""
|
||||
aggregated: Dict[Tuple[str, str], int] = {}
|
||||
for row in rows:
|
||||
if row["is_spare"] == "true":
|
||||
continue
|
||||
part = parts_catalog[row["part_num"]]
|
||||
if part["part_cat_id"] != STICKER_CATEGORY_ID:
|
||||
continue
|
||||
key = (row["set_num"], row["part_num"])
|
||||
aggregated[key] = aggregated.get(key, 0) + int(row["quantity_in_set"])
|
||||
return aggregated
|
||||
|
||||
|
||||
def build_sticker_parts(
|
||||
parts_filtered_path: Path,
|
||||
parts_catalog_path: Path,
|
||||
sets_path: Path,
|
||||
) -> List[dict]:
|
||||
"""Construit la liste des planches d'autocollants par set."""
|
||||
rows = read_rows(parts_filtered_path)
|
||||
parts_catalog = load_parts_catalog(parts_catalog_path)
|
||||
sets_lookup = load_sets(sets_path)
|
||||
aggregated = aggregate_stickers(rows, parts_catalog)
|
||||
stickers: List[dict] = []
|
||||
for (set_num, part_num), quantity in aggregated.items():
|
||||
set_row = sets_lookup[set_num]
|
||||
part = parts_catalog[part_num]
|
||||
stickers.append(
|
||||
{
|
||||
"set_num": set_num,
|
||||
"set_id": set_row["set_id"],
|
||||
"year": set_row["year"],
|
||||
"name": set_row["name"],
|
||||
"part_num": part_num,
|
||||
"part_name": part["name"],
|
||||
"quantity": str(quantity),
|
||||
}
|
||||
)
|
||||
stickers.sort(key=lambda r: (int(r["year"]), r["set_num"], r["part_num"]))
|
||||
return stickers
|
||||
|
||||
|
||||
def write_sticker_parts(destination_path: Path, rows: Iterable[dict]) -> None:
|
||||
"""Écrit le CSV des autocollants par set."""
|
||||
ensure_parent_dir(destination_path)
|
||||
fieldnames = ["set_num", "set_id", "year", "name", "part_num", "part_name", "quantity"]
|
||||
with destination_path.open("w", newline="") as csv_file:
|
||||
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
Reference in New Issue
Block a user