Produire des images de remplacement

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

Pas besoin d’IA

GratuitInstantanéVos données ne sortent pas

Le verdict

L’échelle de cette fiche n’a qu’un seul niveau, 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.

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

Pas besoin d’IA

Règle et algorithme classiqueN0GratuitInstantané

Python

snippets/generate-placeholder-images/n0.pyTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
"""
Une image de remplacement sans modèle : un SVG dérivé d'un hachage de
l'identifiant.

Niveau N0. Aucun fichier écrit et aucun octet téléchargé : la fonction
renvoie une chaîne de balisage qu'un gabarit peut incorporer ou qu'un
gestionnaire de requête peut servir.

Deux propriétés portent toute l'approche.

Elle est déterministe. Le même identifiant donne toujours exactement la même
image, en Python comme en JavaScript, tant que ce code reste inchangé.

Son arithmétique est entière. La teinte, le chroma et les positions des
cellules sont calculés sans une seule division qui pourrait arrondir
autrement d'un environnement d'exécution à l'autre, et c'est ce qui permet à
la version Python et à la version JavaScript de s'accorder au caractère près.
"""

import re

# 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

# Characters XML 1.0 does not allow anywhere in a document, escaped or not: C0
# controls other than tab and line breaks, lone surrogates, U+FFFE and U+FFFF.
NOT_XML = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\ud800-\udfff\ufffe\uffff]")

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, fed code points rather than UTF-8 bytes: identical to
    the reference on ASCII, different from it on any other character.

    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.

    Escaping is not enough: a control character pasted from a spreadsheet has
    no escaped form in XML 1.0 and would make the whole SVG unreadable, so it
    is dropped. The ampersand goes first, or it would escape the escapes.
    """
    text = NOT_XML.sub("", text)
    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.jsTesté tel quel — Cet extrait s’exécute avec ses vraies dépendances, et son test tourne à chaque construction du site.
/**
 * Une image de remplacement sans modèle : un SVG dérivé d'un hachage de
 * l'identifiant.
 *
 * Niveau N0. Aucun fichier écrit et aucun octet téléchargé : la fonction
 * renvoie une chaîne de balisage qu'un gabarit peut incorporer ou qu'un
 * gestionnaire de requête peut servir.
 *
 * Deux propriétés portent toute l'approche.
 *
 * Elle est déterministe. Le même identifiant donne toujours exactement la même
 * image, en Python comme en JavaScript, tant que ce code reste inchangé.
 *
 * Son arithmétique est entière. La teinte, le chroma et les positions des
 * cellules sont calculés sans une seule division qui pourrait arrondir
 * autrement d'un environnement d'exécution à l'autre, et c'est ce qui permet à
 * la version Python et à la version JavaScript de s'accorder au caractère près.
 */

// 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;

// Characters XML 1.0 does not allow anywhere in a document, escaped or not: C0
// controls other than tab and line breaks, lone surrogates, U+FFFE and U+FFFF.
const NOT_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/gu;

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

/**
 * FNV-1a on 32 bits, fed code points rather than UTF-8 bytes: identical to
 * the reference on ASCII, different from it on any other character.
 *
 * 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.
 *
 * Escaping is not enough: a control character pasted from a spreadsheet has
 * no escaped form in XML 1.0 and would make the whole SVG unreadable, so it
 * is dropped. The ampersand goes first, or it would escape the escapes.
 */
function escape(text) {
  return text.replace(NOT_XML, '').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('');
}

Essayer

interactif

Le code ci-dessus tourne dans votre navigateur. Écrivez dans le champ : le résultat se refait sous vos yeux.

Tapez un identifiant : une référence produit, une adresse, un nom de client. La même chaîne donne toujours exactement la même image.

à vous d’écrire

Recalculé à chaque frappe.

Ou partez d’un exemple

La saisie libre et les autres exemples ont besoin de JavaScript. L’exemple affiché ci-dessous, lui, a été calculé à la construction du site.

Ce que le code en fait

Ce qu’il renvoie

Teinte 44° — jaune

24 rectangles pleins en 2 couleurs, et rien à télécharger : le balisage entier tient dans la page.

L’image produite

Image de remplacement pour « Boulangerie Martin »

« Boulangerie Martin » donnera cette image-là, en JavaScript comme en Python, tant que ce code ne change pas.

Ce que ça implique

  • Vos données ne sortent pas
  • Résultat toujours identique
  • Se teste unitairement, cas par cas
  • Aucun fournisseur dans la boucle
  • Empreinte négligeable

Ce que cela ajoute à votre 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 passer au niveau suivant

Il n’y a pas de niveau 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.

Les quatre options

  1. RecommandéN0SVG déterministe dérivé d’un hachage de l’identifiantGratuit · Instantané
  2. N1Niveau sans objet ici
  3. N2Niveau sans objet ici
  4. N3Niveau sans objet ici

Les autres options

N1Niveau sans objet ici

Niveau sans objet ici

Modèle classique légerN1

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.

N2Niveau sans objet ici

Niveau sans objet ici

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

Un modèle de diffusion auto-hébergé pour produire quelques rectangles colorés est l’exemple même du sur-dimensionnement, et il perd la propriété dont cette fiche a besoin : PyTorch, sur lequel repose la bibliothèque de diffusion publiée par huggingface, ne garantit pas des résultats reproductibles d’une version à l’autre, d’une plateforme à l’autre, ni entre processeur et carte graphique, même à graine identique. Il produit en plus ce qu’un remplaçant ne doit pas être : une image vraisemblable de l’objet absent.

N3Niveau sans objet ici

Niveau sans objet ici

API de LLM généralisteN3

Même disproportion, et deux défauts de plus : chaque vignette manquante devient un appel facturé chez un tiers, et le modèle qui la dessine a une date de fin. Le fournisseur pris en exemple a arrêté DALL·E 2 et DALL·E 3 le 12 mai 2026 : un catalogue dont les remplaçants venaient de ces modèles ne peut plus les redemander.

Pour aller plus loin

Source des chiffres

Métadonnées