Coller les dalles directement dans le canevas habillé (pic mémoire divisé par 2)

This commit is contained in:
Antoine Jacquin
2026-09-18 21:22:53 +02:00
parent 5e13151998
commit 93d1656d9f

View File

@ -138,10 +138,14 @@ def _fmt_scale(v):
return f"{v / 1000:g} km" if v >= 1000 else f"{v} m"
def _build_mosaic(images, viz_key, tile_px, n_cols, n_rows, col_min, row_max):
"""Colle les dalles bord à bord — aucun trait, aucun espace entre elles."""
def _build_mosaic_into(canvas, origin_y, images, viz_key, tile_px,
n_cols, n_rows, col_min, row_max):
"""Colle les dalles bord à bord dans un canevas existant (offset vertical).
Aucun trait, aucun espace entre dalles. Coller directement dans le canevas
final évite de garder deux mosaïques pleine résolution en mémoire.
"""
from PIL import Image
canvas = Image.new("RGB", (n_cols * tile_px, n_rows * tile_px), "white")
for (col, row), per_viz in images.items():
img = Image.open(str(per_viz[viz_key]))
if img.mode != "RGB":
@ -151,9 +155,17 @@ def _build_mosaic(images, viz_key, tile_px, n_cols, n_rows, col_min, row_max):
# Dalle (col, row) : X ∈ [col, col+1] km, Y ∈ [row-1, row] km.
# Ligne 0 de l'image = nord → les rows décroissantes descendent.
x = (col - col_min) * tile_px
y = (row_max - row) * tile_px
y = origin_y + (row_max - row) * tile_px
canvas.paste(img, (x, y))
img.close()
def _build_mosaic(images, viz_key, tile_px, n_cols, n_rows, col_min, row_max):
"""Colle les dalles bord à bord dans un canevas neuf dédié à la mosaïque."""
from PIL import Image
canvas = Image.new("RGB", (n_cols * tile_px, n_rows * tile_px), "white")
_build_mosaic_into(canvas, 0, images, viz_key, tile_px, n_cols, n_rows,
col_min, row_max)
return canvas
@ -221,23 +233,22 @@ def _draw_legend(out, draw, legend, layout, y_base):
y += layout["line_h"]
def _decorate(mosaic, m_per_px, title, subtitle, legend=None):
"""Ajoute un bandeau de titre au-dessus et un pied (échelle + nord) dessous.
Avec `legend` (entrée de VIZ_LEGENDS), un bandeau explicatif de la couche
est ajouté sous le pied : titre, dégradé de la colormap avec bornes,
lecture du rendu et méthode de calcul.
L'habillage ne touche jamais la mosaïque : les dalles restent jointives.
"""
from PIL import Image, ImageDraw
w, h = mosaic.size
def _chrome_layout(w, h, legend):
"""Dimensions des bandeaux (titre, pied, légende) autour d'une mosaïque w×h."""
from PIL import Image
top_h = max(90, h // 16)
foot_h = max(84, h // 20)
layout = _legend_layout(legend, mosaic, w) if legend else None
# Image factice pour la mesure : seule la police compte, pas le contenu
layout = _legend_layout(legend, Image.new("RGB", (1, 1)), w) if legend else None
legend_h = layout["height"] if layout else 0
out = Image.new("RGB", (w, h + top_h + foot_h + legend_h), "white")
out.paste(mosaic, (0, top_h))
return top_h, foot_h, layout, legend_h
def _draw_chrome(out, mosaic_w, mosaic_h, top_h, foot_h, m_per_px, title,
subtitle, layout, legend):
"""Dessine sur `out` les bandeaux autour de la zone mosaïque (offset top_h)."""
from PIL import ImageDraw
w, h = mosaic_w, mosaic_h
d = ImageDraw.Draw(out)
title_font = _load_font(max(22, int(top_h * 0.34)))
@ -275,6 +286,24 @@ def _decorate(mosaic, m_per_px, title, subtitle, legend=None):
y_base = h + top_h + foot_h
d.line((0, y_base, w, y_base), fill=(210, 210, 210), width=1)
_draw_legend(out, d, legend, layout, y_base)
def _decorate(mosaic, m_per_px, title, subtitle, legend=None):
"""Ajoute un bandeau de titre au-dessus et un pied (échelle + nord) dessous.
Avec `legend` (entrée de VIZ_LEGENDS), un bandeau explicatif de la couche
est ajouté sous le pied : titre, dégradé de la colormap avec bornes,
lecture du rendu et méthode de calcul.
L'habillage ne touche jamais la mosaïque : les dalles restent jointives.
"""
from PIL import Image
w, h = mosaic.size
top_h, foot_h, layout, legend_h = _chrome_layout(w, h, legend)
out = Image.new("RGB", (w, h + top_h + foot_h + legend_h), "white")
out.paste(mosaic, (0, top_h))
_draw_chrome(out, w, h, top_h, foot_h, m_per_px, title, subtitle,
layout, legend)
return out
@ -374,20 +403,36 @@ def build_export(vis_dir, cells, viz_keys, resolution, fmt, out_dir,
files = [] # fichiers écrits (1 par couche en image, le PDF sinon)
try:
for key in viz_keys:
mosaic = _build_mosaic(images, key, tile_px, n_cols, n_rows,
col_min, row_max)
m_per_px = resolution
caps = [c for c in (int(max_side), page_cap) if c]
cap = min(caps) if caps else 0
if cap and max(mosaic.size) > cap:
ratio = cap / max(mosaic.size)
size = (max(1, round(mosaic.size[0] * ratio)),
max(1, round(mosaic.size[1] * ratio)))
w = n_cols * tile_px
h = n_rows * tile_px
if cap and max(w, h) > cap:
# Mosaïque réduite (téléphone / pages PDF) : taille bornée par
# le cap, le chemin mosaïque → resize → habillage reste simple
ratio = cap / max(w, h)
size = (max(1, round(w * ratio)), max(1, round(h * ratio)))
mosaic = _build_mosaic(images, key, tile_px, n_cols, n_rows,
col_min, row_max)
mosaic = mosaic.resize(size, resample)
m_per_px = resolution / ratio
decorated = _decorate(mosaic, m_per_px, VIZ_LABELS.get(key, key),
subtitle, legend=VIZ_LEGENDS.get(key))
mosaic.close()
decorated = _decorate(mosaic, m_per_px,
VIZ_LABELS.get(key, key), subtitle,
legend=VIZ_LEGENDS.get(key))
mosaic.close()
else:
# Pleine résolution : collage DIRECT dans le canevas habillé —
# jamais deux mosaïques entières en mémoire (la garde
# _MAX_CANVAS_BYTES correspond alors à la taille réelle)
legend = VIZ_LEGENDS.get(key)
top_h, foot_h, layout, legend_h = _chrome_layout(w, h, legend)
decorated = Image.new(
"RGB", (w, h + top_h + foot_h + legend_h), "white")
_build_mosaic_into(decorated, top_h, images, key, tile_px,
n_cols, n_rows, col_min, row_max)
_draw_chrome(decorated, w, h, top_h, foot_h, m_per_px,
VIZ_LABELS.get(key, key), subtitle, layout, legend)
if fmt != "pdf":
key_file = out_dir / export_filename(cells, resolution, [key], fmt)
decorated.save(str(key_file), **save_kw)