Picarones / picarones /report /assets.py
Claude
fix(audit-2): 5 correctifs supplémentaires d'un 2e tour d'audit
b80bb93 unverified
Raw
History Blame
7.46 kB
"""Chargement et préparation des assets du rapport HTML.
Ce module concentre tout ce qui touche aux ressources binaires
embarquées ou référencées par le rapport :
- ``load_vendor_js`` lit un fichier JS vendorisé (Chart.js, etc.).
- ``encode_image_b64`` redimensionne et encode une image en data-URI.
- ``encode_images_b64_from_result`` itère sur un BenchmarkResult.
- ``externalize_images_to_dir`` écrit les images sur disque à côté
du HTML (mode ``--lazy-images`` du Sprint A5).
Extrait de ``picarones/report/generator.py`` lors du sprint de
découpage : isole l'I/O image et vendor du reste de l'orchestration.
"""
from __future__ import annotations
import base64
import io
import logging
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from picarones.core.results import BenchmarkResult
logger = logging.getLogger(__name__)
#: Dossier où sont stockées les ressources JS embarquées.
_VENDOR_DIR = Path(__file__).parent / "vendor"
def load_vendor_js(name: str) -> str:
"""Lit un fichier JS vendorisé et retourne son contenu.
Si le fichier n'existe pas, retourne un commentaire JS qui
garde le rapport valide (pas de SyntaxError côté navigateur).
"""
p = _VENDOR_DIR / name
if p.exists():
return p.read_text(encoding="utf-8")
return f"/* vendor/{name} non trouvé */"
def encode_image_b64(image_path: str, max_width: int = 1200) -> str:
"""Lit une image, la redimensionne si besoin, et retourne un data-URI base64.
Retourne ``""`` si l'image est introuvable ou si l'encodage
échoue (Pillow indisponible, format non géré, fichier corrompu).
Logue un avertissement dans ce dernier cas — le rapport reste
fonctionnel mais l'image manquera dans la galerie.
Distingue ``ImportError`` (Pillow non installée — problème
d'environnement) du reste (problème par image) pour aider au
diagnostic en logs de production.
"""
p = Path(image_path)
if not p.exists():
return ""
try:
from PIL import Image
except ImportError as exc:
logger.warning(
"[report] Pillow indisponible : %s — toutes les images "
"du rapport seront omises. Installer ``pip install Pillow`` "
"ou ``pip install picarones[report]``.",
exc,
)
return ""
try:
with Image.open(p) as img:
if img.width > max_width:
ratio = max_width / img.width
new_h = max(1, int(img.height * ratio))
img = img.resize((max_width, new_h), Image.LANCZOS)
# Convertir en RGB pour éviter les problèmes de mode (RGBA, palette…)
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
buf = io.BytesIO()
fmt = "JPEG" if p.suffix.lower() in (".jpg", ".jpeg") else "PNG"
img.save(buf, format=fmt, optimize=True, quality=85)
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
mime = "image/jpeg" if fmt == "JPEG" else "image/png"
return f"data:{mime};base64,{b64}"
except Exception as exc: # noqa: BLE001 — fallback gracieux + warning
logger.warning(
"[report] échec d'encodage base64 de l'image %s : %s — "
"le rapport ignorera cette image",
image_path,
exc,
)
return ""
def encode_images_b64_from_result(
benchmark: "BenchmarkResult", max_width: int = 1200,
) -> dict[str, str]:
"""Encode toutes les images d'un BenchmarkResult en base64.
Returns
-------
dict
``{doc_id: data_uri}``
"""
images: dict[str, str] = {}
if not benchmark.engine_reports:
return images
for dr in benchmark.engine_reports[0].document_results:
if dr.image_path and dr.doc_id not in images:
uri = encode_image_b64(dr.image_path, max_width=max_width)
if uri:
images[dr.doc_id] = uri
return images
def externalize_images_to_dir(
benchmark: "BenchmarkResult",
output_dir: Path,
max_width: int = 1200,
asset_subdir: str = "report-assets",
) -> dict[str, str]:
"""Sprint A5 (item M-16) — écrit les images sur disque dans un
sous-dossier à côté du HTML, et retourne ``{doc_id: url_relative}``.
Mode « lazy loading » : au lieu d'embarquer chaque image en
base64 dans le HTML (50 MB+ pour un corpus de 100 documents,
~200 MB+ pour 1 000 documents), on les externalise en fichiers
PNG/JPEG locaux. Le HTML les référence via
``<img src="report-assets/…">`` avec ``loading="lazy"`` côté
navigateur.
Le rapport reste auto-portant si l'utilisateur copie le dossier
``report-assets/`` à côté du HTML (cf. CLI ``--lazy-images``).
Parameters
----------
benchmark:
Résultat de benchmark (lit ``image_path`` de chaque DocumentResult).
output_dir:
Dossier où le HTML sera écrit ; le sous-dossier d'assets sera
créé à côté.
max_width:
Largeur max du redimensionnement (cohérent avec
``encode_image_b64``).
asset_subdir:
Nom du sous-dossier d'assets (défaut ``"report-assets"``).
Returns
-------
dict[str, str]
``{doc_id: "report-assets/<doc_id>.png"}`` (URL relative
consommable directement dans un attribut HTML ``src``).
"""
from PIL import Image
assets_dir = output_dir / asset_subdir
assets_dir.mkdir(parents=True, exist_ok=True)
out: dict[str, str] = {}
seen_ids: set[str] = set()
for engine_report in benchmark.engine_reports:
for dr in engine_report.document_results:
doc_id = dr.doc_id
if doc_id in seen_ids:
continue
seen_ids.add(doc_id)
try:
src = Path(dr.image_path)
if not src.exists():
continue
# Nom de fichier dérivé du doc_id, normalisé sans
# caractères dangereux pour le filesystem.
safe_id = "".join(
c if c.isalnum() or c in "._-" else "_" for c in doc_id
)
dest = assets_dir / f"{safe_id}{src.suffix.lower() or '.png'}"
with Image.open(src) as img:
if img.width > max_width:
ratio = max_width / img.width
new_h = max(1, int(img.height * ratio))
img = img.resize((max_width, new_h), Image.LANCZOS)
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
fmt = "JPEG" if dest.suffix in (".jpg", ".jpeg") else "PNG"
img.save(dest, format=fmt, optimize=True, quality=85)
# URL relative (POSIX style même sur Windows pour HTML).
out[doc_id] = f"{asset_subdir}/{dest.name}"
except Exception as exc: # noqa: BLE001 — fallback silencieux + warning
logger.warning(
"[report] échec d'externalisation de l'image %s : %s — "
"le rapport ignorera cette image",
dr.image_path,
exc,
)
return out
__all__ = [
"load_vendor_js",
"encode_image_b64",
"encode_images_b64_from_result",
"externalize_images_to_dir",
]