Catalogue Générer

Produire des images de remplacement

Afficher une image provisoire là où la vraie manque encore, dans une maquette ou un catalogue incomplet.

RecommandéN0 Révisée le

Récapitulatif des barreaux
Barreau Approche Coût Latence Données Déterministe Verdict
Règle et algorithme classique N0 — Règle et algorithme classique SVG déterministe dérivé d'un hachage de l'identifiant Nul <1 ms Rien ne sort Oui Recommandé
Modèle classique léger N1 — Modèle classique léger Barreau absent Il n'y a rien à apprendre. Un modèle classique s'entraîne à prédire une bonne réponse à partir d'exemples ; ici aucune image n'est plus juste qu'une autre pour un identifiant donné, la sortie voulue étant décorative et arbitraire. Il n'existe donc pas de jeu étiqueté possible, et un modèle appris n'ajouterait qu'un fichier de poids à transporter là où une fonction de hachage donne déjà une réponse stable.
Petit modèle spécialisé auto-hébergé N2 — Petit modèle spécialisé auto-hébergé Barreau absent Un modèle de diffusion auto-hébergé pour produire quelques rectangles colorés est l'exemple même du sur-dimensionnement : un service permanent, un accélérateur graphique, et une image dont on ne retrouve pas l'exacte réplique en changeant de version de bibliothèque ou de pilote. Il produit en plus ce qu'un remplaçant ne doit pas être — une image vraisemblable de l'objet absent, que le lecteur prendra pour la vraie.
API de LLM généraliste N3 — API de LLM généraliste Barreau absent Même disproportion, et deux défauts de plus : chaque vignette manquante devient un appel facturé chez un tiers, et rien ne garantit que le même identifiant redonnera la même image, le modèle du fournisseur changeant sous vos pieds. Un catalogue rendu de nouveau le mois suivant n'aurait plus les mêmes remplaçants.

N0 — Règle et algorithme classique Règle et algorithme classique Recommandé

SVG déterministe dérivé d'un hachage de l'identifiant

Coût
Nul
Latence
<1 ms

Preuve d’exécution : Code exécuté tel quel

Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.

Python

snippets/generate-placeholder-images/n0.py
"""
A placeholder image without a model: an SVG derived from a hash of the identifier.

Rung N0. No file is written and no byte is downloaded: the function returns a
string of markup that a template can inline or a handler can serve.

Two properties carry the whole approach.

It is deterministic. The same identifier always yields exactly the same image,
on every machine, in every language, for ever. A placeholder that changed on
each render would flicker in a grid and defeat every HTTP cache.

Its arithmetic is integer. Hue, chroma and cell positions are computed without
a single division that could round differently from one runtime to another,
which is what lets the Python and the JavaScript version agree character for
character.
"""

# FNV-1a, the same constants as the rest of the catalogue, so that identifiers
# hash identically wherever they are hashed.
FNV_OFFSET = 2166136261
FNV_PRIME = 16777619
MASK32 = 0xFFFFFFFF

GRID = 5  # cells per side
COLUMNS = 3  # independent columns; the remaining two mirror them


def stable_hash(text: str) -> int:
    """
    FNV-1a on 32 bits.

    Not the built-in hash: that one is salted per process, so it would give a
    different image every time the server restarted.
    """
    digest = FNV_OFFSET
    for char in text:
        digest = ((digest ^ ord(char)) * FNV_PRIME) & MASK32
    return digest


def _hex_colour(hue: int, saturation: int, lightness: int) -> str:
    """HSL to hexadecimal, in integers only, all three arguments in percent."""
    chroma = (255 * saturation * (100 - abs(2 * lightness - 100))) // 10000
    edge = (chroma * (60 - abs((hue % 120) - 60))) // 60
    floor = (255 * lightness) // 100 - chroma // 2
    wheel = ((chroma, edge, 0), (edge, chroma, 0), (0, chroma, edge),
             (0, edge, chroma), (edge, 0, chroma), (chroma, 0, edge))
    red, green, blue = wheel[(hue // 60) % 6]
    return "#%02x%02x%02x" % (red + floor, green + floor, blue + floor)


def _escape(text: str) -> str:
    """
    The identifier ends up inside an attribute, so it is markup until escaped.

    The ampersand goes first, or it would escape the escapes.
    """
    text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    return text.replace('"', "&quot;")


def placeholder_svg(identifier: str, size: int = 240) -> str:
    """
    Build a symmetric two-tone figure on a tinted ground.

    The high bits of the hash choose the hue, the low ones switch cells on and
    off. Mirroring the left columns onto the right ones costs one line and is
    what makes the result read as a mark rather than as noise.
    """
    digest = stable_hash(identifier)
    hue = (digest >> 16) % 360
    cell = size // GRID
    margin = (size - cell * GRID) // 2

    parts = [
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}"'
        f' viewBox="0 0 {size} {size}" role="img" aria-label="{_escape(identifier)}">',
        f'<rect width="{size}" height="{size}" fill="{_hex_colour(hue, 45, 90)}"/>',
    ]
    ink = _hex_colour(hue, 55, 42)
    for row in range(GRID):
        for column in range(GRID):
            mirrored = min(column, GRID - 1 - column)
            if not (digest >> (row * COLUMNS + mirrored)) & 1:
                continue
            x = margin + column * cell
            y = margin + row * cell
            parts.append(
                f'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" fill="{ink}"/>'
            )
    parts.append("</svg>")
    return "".join(parts)

JavaScript

snippets/generate-placeholder-images/n0.js
/**
 * A placeholder image without a model: an SVG derived from a hash of the identifier.
 *
 * Rung N0. No file is written and no byte is downloaded: the function returns
 * a string of markup that a template can inline or a handler can serve.
 *
 * Two properties carry the whole approach.
 *
 * It is deterministic. The same identifier always yields exactly the same
 * image, on every machine, in every language, for ever. A placeholder that
 * changed on each render would flicker in a grid and defeat every HTTP cache.
 *
 * Its arithmetic is integer. Hue, chroma and cell positions are computed
 * without a single division that could round differently from one runtime to
 * another, which is what lets the JavaScript and the Python version agree
 * character for character.
 */

// FNV-1a, the same constants as the rest of the catalogue, so that identifiers
// hash identically wherever they are hashed.
const FNV_OFFSET = 2166136261;
const FNV_PRIME = 16777619;

const GRID = 5; // cells per side
const COLUMNS = 3; // independent columns; the remaining two mirror them

/**
 * FNV-1a on 32 bits.
 *
 * Math.imul is what keeps it exact: a plain multiplication on numbers this
 * large loses precision past 2^53 and would silently drift away from the
 * integers Python computes.
 */
export function stableHash(text) {
  let digest = FNV_OFFSET;
  for (const char of text) {
    digest = Math.imul(digest ^ char.codePointAt(0), FNV_PRIME) >>> 0;
  }
  return digest;
}

/** HSL to hexadecimal, in integers only, all three arguments in percent. */
function hexColour(hue, saturation, lightness) {
  const chroma = Math.floor((255 * saturation * (100 - Math.abs(2 * lightness - 100))) / 10000);
  const edge = Math.floor((chroma * (60 - Math.abs((hue % 120) - 60))) / 60);
  const floor = Math.floor((255 * lightness) / 100) - Math.floor(chroma / 2);
  const wheel = [[chroma, edge, 0], [edge, chroma, 0], [0, chroma, edge],
    [0, edge, chroma], [edge, 0, chroma], [chroma, 0, edge]];
  const channels = wheel[Math.floor(hue / 60) % 6];
  return `#${channels.map((v) => (v + floor).toString(16).padStart(2, '0')).join('')}`;
}

/**
 * The identifier ends up inside an attribute, so it is markup until escaped.
 *
 * The ampersand goes first, or it would escape the escapes.
 */
function escape(text) {
  return text.replaceAll('&', '&amp;').replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;').replaceAll('"', '&quot;');
}

/**
 * Build a symmetric two-tone figure on a tinted ground.
 *
 * The high bits of the hash choose the hue, the low ones switch cells on and
 * off. Mirroring the left columns onto the right ones costs one line and is
 * what makes the result read as a mark rather than as noise.
 */
export function placeholderSvg(identifier, size = 240) {
  const digest = stableHash(identifier);
  const hue = (digest >>> 16) % 360;
  const cell = Math.floor(size / GRID);
  const margin = Math.floor((size - cell * GRID) / 2);

  const parts = [
    `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"` +
      ` viewBox="0 0 ${size} ${size}" role="img" aria-label="${escape(identifier)}">`,
    `<rect width="${size}" height="${size}" fill="${hexColour(hue, 45, 90)}"/>`,
  ];
  const ink = hexColour(hue, 55, 42);
  for (let row = 0; row < GRID; row += 1) {
    for (let column = 0; column < GRID; column += 1) {
      const mirrored = Math.min(column, GRID - 1 - column);
      if (!((digest >>> (row * COLUMNS + mirrored)) & 1)) continue;
      const x = margin + column * cell;
      const y = margin + row * cell;
      parts.push(`<rect x="${x}" y="${y}" width="${cell}" height="${cell}" fill="${ink}"/>`);
    }
  }
  parts.push('</svg>');
  return parts.join('');
}

Risques

Sortie de données
Rien ne sort
Déterminisme
Oui
Testabilité
Testable unitairement
Dépendance fournisseur
Aucune
Empreinte
Négligeable
Périmètre réglementaire
  • Aucun périmètre spécifique ajouté : l'image est calculée dans votre processus, rien n'est téléchargé et rien n'est transmis
  • L'identifiant reçu est recopié dans l'attribut aria-label du balisage servi : ce qu'il contient est publié avec la page

Point de rupture

L'image ne dit rien de ce qu'elle remplace : le hachage décide de tout, le sens de l'identifiant de rien. Le test le montre sur trois points — « red velvet sofa » ne tombe pas dans les rouges, sofa-1 et sofa-2 reçoivent des teintes éloignées alors qu'il s'agit du même canapé, et la sortie n'est jamais que des rectangles pleins en deux couleurs.

Quand monter d’un barreau

Il n'y a pas de barreau au-dessus. Le jour où il vous faut une vraie photographie de l'objet, ce n'est plus un remplaçant qu'on vous demande, et aucune approche de cette fiche n'y répond.

N1 — Modèle classique léger Modèle classique léger

Barreau absent

Il n'y a rien à apprendre. Un modèle classique s'entraîne à prédire une bonne réponse à partir d'exemples ; ici aucune image n'est plus juste qu'une autre pour un identifiant donné, la sortie voulue étant décorative et arbitraire. Il n'existe donc pas de jeu étiqueté possible, et un modèle appris n'ajouterait qu'un fichier de poids à transporter là où une fonction de hachage donne déjà une réponse stable.

N2 — Petit modèle spécialisé auto-hébergé Petit modèle spécialisé auto-hébergé

Barreau absent

Un modèle de diffusion auto-hébergé pour produire quelques rectangles colorés est l'exemple même du sur-dimensionnement : un service permanent, un accélérateur graphique, et une image dont on ne retrouve pas l'exacte réplique en changeant de version de bibliothèque ou de pilote. Il produit en plus ce qu'un remplaçant ne doit pas être — une image vraisemblable de l'objet absent, que le lecteur prendra pour la vraie.

N3 — API de LLM généraliste API de LLM généraliste

Barreau absent

Même disproportion, et deux défauts de plus : chaque vignette manquante devient un appel facturé chez un tiers, et rien ne garantit que le même identifiant redonnera la même image, le modèle du fournisseur changeant sous vos pieds. Un catalogue rendu de nouveau le mois suivant n'aurait plus les mêmes remplaçants.

Le verdict

RecommandéN0

L'échelle de cette fiche n'a qu'un barreau, et c'est son intérêt : le besoin est décoratif et arbitraire, ce qui est précisément le cas où une fonction de hachage donne la réponse exacte plutôt qu'une approximation. Rien ne sort du processus, le coût marginal est nul, et le résultat se teste unitairement au caractère près — le même balisage attendu figure en toutes lettres dans le test Python et dans le test JavaScript, ce qui épingle les deux implémentations l'une à l'autre. La sortie de cette fiche ne se fait pas par le haut : elle se fait en obtenant la vraie image.

Pour aller plus loin

Métadonnées