Export Web/SCORM : inline les icônes en data: URI (CORS Firefox/file://)
Le chemin relatif ("icons/x.svg") était enfin correct, mais Firefox
bloque encore par CORS TOUT chemin de fichier pour mask-image sous
file:// — chaque ressource file:// y est une origine opaque distincte,
indépendamment du chemin (troisième rapport du même utilisateur). Seule
une data: URI (aucune requête réseau séparée) contourne le problème.
_build_icon_data_uris (build_scorm_package.py) encode chaque icône
RÉELLEMENT posée dans le jeu (élément direct ou niché dans un élément de
jeu réutilisable) en base64, ajouté au payload sous
gameData.icon_data_uris ; forgeRenderIcone (JS) le consulte pour toute
régénération après une action, avec repli sur l'ancien chemin relatif
si jamais une icône n'a pas été pré-encodée.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b60fe63a54
commit
82d00b3800
@@ -7,6 +7,7 @@ SCORM tourne SEUL dans le LMS du client, sans serveur Forge Engine
|
||||
disponible. Rien de commun avec le zip .exe hors le sous-ensemble de
|
||||
static/ et _copy_characters (mêmes assets, mêmes règles de copie
|
||||
sélective) — réutilisés depuis build_package plutôt que dupliqués."""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -71,14 +72,15 @@ def _relativize_absolute_urls(html, slug):
|
||||
|
||||
Cas particulier des icônes (render_icone.py) : leur URL est posée dans
|
||||
un style INLINE (--icon-url), mais CONSUMÉE par `mask-image:
|
||||
var(--icon-url)` dans static/style.css (voir .icon-svg) — une URL
|
||||
relative dans une PROPRIÉTÉ PERSONNALISÉE CSS se résout par rapport à
|
||||
la feuille de style où le var() est UTILISÉ, pas où elle est définie
|
||||
(piège CSS connu). "icons/x.svg" (juste "/static/" retiré, sans le
|
||||
garder) reste donc correct une fois résolu depuis static/style.css,
|
||||
alors que "static/icons/x.svg" y redevient "static/static/icons/x.svg"
|
||||
(bug signalé — CORS bloqué sur ce chemin doublé). Traité EN PREMIER,
|
||||
plus spécifique que le remplacement générique juste après."""
|
||||
var(--icon-url)` dans static/style.css (voir .icon-svg) — un chemin
|
||||
RELATIF n'y suffit PAS (voir _build_icon_data_uris plus bas, qui
|
||||
remplace ces chemins par des data: URI juste après cet appel) : Firefox
|
||||
traite chaque ressource file:// comme une origine opaque distincte, et
|
||||
mask-image y déclenche un fetch en mode CORS quel que soit le chemin.
|
||||
"icons/x.svg" reste néanmoins la forme intermédiaire correcte que
|
||||
_build_icon_data_uris recherche pour remplacer (elle-même résolue par
|
||||
rapport à static/style.css, jamais index.html — piège CSS connu :
|
||||
"static/icons/x.svg" y redeviendrait "static/static/icons/x.svg")."""
|
||||
return (
|
||||
html.replace(f"/game/{slug}/uploads/", "uploads/")
|
||||
.replace("/static/icons/", "icons/")
|
||||
@@ -86,6 +88,65 @@ def _relativize_absolute_urls(html, slug):
|
||||
)
|
||||
|
||||
|
||||
def _collect_icon_slugs(payload):
|
||||
"""Tout _icon_slug (widget "icone") réellement posé dans ce jeu —
|
||||
élément normal ET élément niché dans un élément de jeu réutilisable
|
||||
(element_type_templates, voir full_game_payload.py) — pour ne pas
|
||||
embarquer ("data:" URI, voir _build_icon_data_uris) le catalogue
|
||||
ENTIER (~1 Mo pour ~260 icônes) alors qu'un jeu n'en utilise
|
||||
typiquement que quelques-unes."""
|
||||
slugs = set()
|
||||
|
||||
def scan(elements):
|
||||
for el in elements or []:
|
||||
if el.get("widget") == "icone":
|
||||
slugs.add((el.get("attributes") or {}).get("_icon_slug") or "star")
|
||||
|
||||
for s in payload.get("screens", []):
|
||||
scan(s.get("elements"))
|
||||
for tree in (payload.get("element_type_templates") or {}).values():
|
||||
scan(tree.get("top"))
|
||||
for kids in (tree.get("children_map") or {}).values():
|
||||
scan(kids)
|
||||
return slugs
|
||||
|
||||
|
||||
def _build_icon_data_uris(slugs):
|
||||
"""Encode chaque icône RÉELLEMENT utilisée (voir _collect_icon_slugs)
|
||||
en data: URI. Nécessaire en plus de _relativize_absolute_urls : Firefox
|
||||
traite CHAQUE ressource file:// comme une origine opaque distincte, et
|
||||
mask-image y déclenche un fetch en mode CORS même pour un chemin
|
||||
relatif par ailleurs tout à fait valide ("Blocage d'une requête
|
||||
multiorigine... la requête CORS n'utilise pas HTTP", signalé par un
|
||||
utilisateur MALGRÉ le chemin déjà corrigé) — aucun chemin de fichier,
|
||||
aussi correct soit-il, ne peut donc fonctionner sous file://. Une
|
||||
data: URI n'est PAS une requête réseau séparée : elle contourne
|
||||
entièrement ce problème (comme pour toute autre resource CSS
|
||||
référencée par chemin sous file://)."""
|
||||
uris = {}
|
||||
icons_dir = os.path.join(_BASE_DIR, "static", "icons")
|
||||
for icon_slug in slugs:
|
||||
path = os.path.join(icons_dir, f"{icon_slug}.svg")
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
with open(path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode("ascii")
|
||||
uris[icon_slug] = f"data:image/svg+xml;base64,{b64}"
|
||||
return uris
|
||||
|
||||
|
||||
def _inline_icon_urls(html, icon_data_uris):
|
||||
"""Remplace chaque "icons/<slug>.svg" (forme relative déjà posée par
|
||||
_relativize_absolute_urls/render_icone.py) par sa data: URI — s'applique
|
||||
aussi bien au rendu initial (déjà bake dans le payload JSON embarqué
|
||||
dans cette même page) qu'à toute régénération faite plus tard côté JS
|
||||
(voir forgeRenderIcone, qui lit directement gameData.icon_data_uris
|
||||
plutôt que de reconstruire un chemin de fichier)."""
|
||||
for icon_slug, uri in icon_data_uris.items():
|
||||
html = html.replace(f"icons/{icon_slug}.svg", uri)
|
||||
return html
|
||||
|
||||
|
||||
def _list_package_files(staging_dir):
|
||||
"""Chemins relatifs de tout ce qui a déjà été écrit dans staging_dir
|
||||
(hors imsmanifest.xml lui-même) — le manifeste doit les lister TOUS
|
||||
@@ -119,6 +180,11 @@ def build_scorm_zip(slug):
|
||||
# seule personne à la fois sur son poste, comme /game/<slug>/play —
|
||||
# jamais d'état "par joueur" à distinguer hors ligne.
|
||||
payload = screens.full_game_payload(slug)
|
||||
# gameData.icon_data_uris (voir forgeRenderIcone,
|
||||
# static/js/play/offline/render-special-widgets.js) : ajouté au
|
||||
# payload AVANT le rendu pour qu'il fasse partie du même payload_json
|
||||
# embarqué, sans passe supplémentaire.
|
||||
payload["icon_data_uris"] = _build_icon_data_uris(_collect_icon_slugs(payload))
|
||||
|
||||
staging_dir = tempfile.mkdtemp(prefix="forge_scorm_")
|
||||
try:
|
||||
@@ -130,6 +196,7 @@ def build_scorm_zip(slug):
|
||||
offline_mode=True, asset_url=lambda filename: "static/" + filename,
|
||||
)
|
||||
html = _relativize_absolute_urls(html, slug)
|
||||
html = _inline_icon_urls(html, payload["icon_data_uris"])
|
||||
with open(os.path.join(staging_dir, "index.html"), "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user