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" 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): def _build_mosaic_into(canvas, origin_y, images, viz_key, tile_px,
"""Colle les dalles bord à bord — aucun trait, aucun espace entre elles.""" 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 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(): for (col, row), per_viz in images.items():
img = Image.open(str(per_viz[viz_key])) img = Image.open(str(per_viz[viz_key]))
if img.mode != "RGB": 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. # Dalle (col, row) : X ∈ [col, col+1] km, Y ∈ [row-1, row] km.
# Ligne 0 de l'image = nord → les rows décroissantes descendent. # Ligne 0 de l'image = nord → les rows décroissantes descendent.
x = (col - col_min) * tile_px 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)) canvas.paste(img, (x, y))
img.close() 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 return canvas
@ -221,23 +233,22 @@ def _draw_legend(out, draw, legend, layout, y_base):
y += layout["line_h"] y += layout["line_h"]
def _decorate(mosaic, m_per_px, title, subtitle, legend=None): def _chrome_layout(w, h, legend):
"""Ajoute un bandeau de titre au-dessus et un pied (échelle + nord) dessous. """Dimensions des bandeaux (titre, pied, légende) autour d'une mosaïque w×h."""
from PIL import Image
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
top_h = max(90, h // 16) top_h = max(90, h // 16)
foot_h = max(84, h // 20) 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 legend_h = layout["height"] if layout else 0
out = Image.new("RGB", (w, h + top_h + foot_h + legend_h), "white") return top_h, foot_h, layout, legend_h
out.paste(mosaic, (0, top_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) d = ImageDraw.Draw(out)
title_font = _load_font(max(22, int(top_h * 0.34))) 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 y_base = h + top_h + foot_h
d.line((0, y_base, w, y_base), fill=(210, 210, 210), width=1) d.line((0, y_base, w, y_base), fill=(210, 210, 210), width=1)
_draw_legend(out, d, legend, layout, y_base) _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 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) files = [] # fichiers écrits (1 par couche en image, le PDF sinon)
try: try:
for key in viz_keys: for key in viz_keys:
mosaic = _build_mosaic(images, key, tile_px, n_cols, n_rows,
col_min, row_max)
m_per_px = resolution m_per_px = resolution
caps = [c for c in (int(max_side), page_cap) if c] caps = [c for c in (int(max_side), page_cap) if c]
cap = min(caps) if caps else 0 cap = min(caps) if caps else 0
if cap and max(mosaic.size) > cap: w = n_cols * tile_px
ratio = cap / max(mosaic.size) h = n_rows * tile_px
size = (max(1, round(mosaic.size[0] * ratio)), if cap and max(w, h) > cap:
max(1, round(mosaic.size[1] * ratio))) # 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) mosaic = mosaic.resize(size, resample)
m_per_px = resolution / ratio m_per_px = resolution / ratio
decorated = _decorate(mosaic, m_per_px, VIZ_LABELS.get(key, key), decorated = _decorate(mosaic, m_per_px,
subtitle, legend=VIZ_LEGENDS.get(key)) VIZ_LABELS.get(key, key), subtitle,
mosaic.close() 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": if fmt != "pdf":
key_file = out_dir / export_filename(cells, resolution, [key], fmt) key_file = out_dir / export_filename(cells, resolution, [key], fmt)
decorated.save(str(key_file), **save_kw) decorated.save(str(key_file), **save_kw)