PerfectVector
Par Irene Kim

SVG vers Canvas : charger le dessin avant l’export PNG

Charge un SVG entier via Image.decode, dessine-le à la taille bitmap choisie, teste CORS et la lecture PNG, et sépare la source modifiable des pixels exportés.

10 min de lecture
Sur cette page

Pour dessiner un SVG entier sur canvas, charge-le comme image, attends image.decode(), puis fournis cette image à drawImage(). Vérifie ensuite l’export PNG séparément : une image peut apparaître sur canvas alors que le navigateur bloque la lecture ou l’enregistrement de ses pixels.

Cette méthode peint le document SVG comme image. Elle ne donne pas au canvas des chemins SVG modifiables. Si tu veux plutôt reconstruire certains contours, utilise le guide SVG avec Path2D. Garde le SVG original quelle que soit la méthode d’affichage choisie.

Sépare décodage, dessin et exportation

Un HTMLImageElement est une entrée acceptée par drawImage() de canvas. Sa largeur et sa hauteur de destination contrôlent la taille peinte. Image.decode() renvoie une promesse résolue lorsque l’image est décodée ; une requête échouée ou un problème de décodage peut la rejeter.

Un décodage réussi n’est que la première vérification. Une image d’une autre origine chargée sans autorisation CORS peut encore être dessinée, mais elle contamine le canvas. La lecture des pixels et l’exportation échouent ensuite. Le guide des images autorisées par CORS de MDN explique cette distinction.

Garde la source, vérifie les pixels
Illustration conceptuelle d’un losange creux et d’un cercle comme contours sources, dessin rendu lisse et sortie sur grille de pixels
Illustration : les contours sources et les pixels peints sont des représentations différentes. Les cadres, flèches et grilles grossières sont conceptuels ; les résultats de navigateur ci-dessous proviennent d’un fichier de test original exécuté indépendamment.

Crée un SVG original autonome

Enregistre ceci sous master.svg. Il contient un chemin et un cercle, sans image intégrée, police ni dépendance externe. Le losange intérieur et le canevas environnant sont transparents dans la source.

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="160" viewBox="0 0 240 160">
  <path d="M96 24 L152 80 L96 136 L40 80 Z M96 58 L118 80 L96 102 L74 80 Z" fill="#176b5b" fill-rule="evenodd"/>
  <circle cx="200" cy="80" r="24" fill="#d4a53a"/>
</svg>

Nous utilisons le fichier complet comme image, avec les deux formes et son viewBox. À la différence d’une extraction de la seule valeur d d’un chemin, cela demande au moteur de rendu d’images SVG du navigateur de peindre le document.

L’exemple est volontairement autonome. SVG utilisé comme image a des restrictions, notamment sur les ressources externes et les scripts ; consulte le guide de SVG comme image de MDN. Ne considère pas le décodage réussi de ce petit fichier comme la preuve qu’un SVG quelconque chargera des polices, styles ou dessins liés externes. Pour choisir la livraison, compare SVG en ligne et élément image.

Exécute deux origines locales avec des réponses CORS différentes

Une origine inclut son port. Le serveur uniquement local suivant expose les mêmes octets SVG via deux origines. Sur la seconde, /cors/master.svg inclut Access-Control-Allow-Origin: * ; /plain/master.svg ne l’inclut pas. Cela isole l’autorisation de réponse du dessin.

Enregistre ceci sous serve-proof.py à côté du SVG et du HTML ci-dessous. Lance python3 serve-proof.py, puis ouvre http://127.0.0.1:8770/starter.html. Les deux ports doivent être libres ; si tu les changes, actualise le serveur et le HTML ensemble. Appuie sur Entrée dans le terminal pour arrêter les deux serveurs.

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from threading import Thread
 
root = Path(__file__).parent
class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=str(root), **kwargs)
    def do_GET(self):
        if self.path in ['/cors/master.svg', '/plain/master.svg']:
            payload = (root / 'master.svg').read_bytes()
            self.send_response(200)
            self.send_header('Content-Type', 'image/svg+xml')
            self.send_header('Cache-Control', 'no-store')
            if self.path.startswith('/cors/'):
                self.send_header('Access-Control-Allow-Origin', '*')
            self.end_headers()
            self.wfile.write(payload)
        else:
            super().do_GET()
 
servers = [ThreadingHTTPServer(('127.0.0.1', port), Handler) for port in (8770, 8783)]
for server in servers:
    Thread(target=server.serve_forever, daemon=True).start()
try:
    input('Original SVG proof on 127.0.0.1:8770 and :8783. Enter to stop.\n')
finally:
    for server in servers:
        server.shutdown()
        server.server_close()

Cet en-tête permissif appartient à un test local qui sert notre propre dessin. Pour un hôte d’images en production, configure l’origine autorisée appropriée sur un serveur que tu contrôles. Définir un attribut d’image n’accorde pas l’accès au contenu d’un autre serveur.

Décode l’image avant de la dessiner

Enregistre cette page complète sous starter.html. Chaque cas possède son propre canvas : le cas contaminé ne peut donc pas contaminer les cas réussis. Définis crossOrigin avant src pour les requêtes nécessitant CORS, comme le décrit la référence de crossOrigin de MDN.

Le bitmap du canvas est explicitement de 480 × 320 ; sa taille d’affichage CSS est de 240 × 160. Les dimensions intrinsèques de l’image originale sont 240 × 160, et drawImage() la met à l’échelle aux dimensions bitmap choisies. Ce sont des dimensions choisies, pas une mesure du rapport de pixels de l’appareil.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Original SVG to canvas export</title>
<style>
  body { margin: 24px; font: 16px system-ui; background: #f6f2e8; }
  main { display: grid; grid-template-columns: repeat(2, 280px); gap: 20px; }
  figure { margin: 0; }
  canvas { display: block; width: 240px; height: 160px; background: white; }
  figcaption { margin: 6px 0; }
  pre { white-space: pre-wrap; font-size: 12px; }
</style>
<h1>Decode, paint, then check PNG access</h1>
<main id="cases"></main>
<pre id="report"></pre>
<script>
  function pngBlob(canvas) {
    return new Promise((resolve, reject) => {
      try { canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('Empty PNG')), 'image/png'); }
      catch (error) { reject(error); }
    });
  }
  function dataURL(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsDataURL(blob);
    });
  }
  async function render(test) {
    const figure = document.createElement('figure');
    const canvas = document.createElement('canvas');
    canvas.width = 480;
    canvas.height = 320;
    const caption = document.createElement('figcaption');
    figure.append(canvas, caption);
    document.getElementById('cases').append(figure);
    const result = { id: test.id, url: new URL(test.src, location.href).href,
      crossOrigin: test.crossOrigin ?? null, canvasWidth: canvas.width, canvasHeight: canvas.height };
    const image = new Image();
    if (test.crossOrigin) image.crossOrigin = test.crossOrigin;
    image.src = result.url;
    try { await image.decode(); }
    catch (error) {
      result.stage = 'decode';
      result.error = error.name;
      caption.textContent = test.id + ': decode ' + error.name;
      return result;
    }
    result.naturalWidth = image.naturalWidth;
    result.naturalHeight = image.naturalHeight;
    const context = canvas.getContext('2d');
    context.drawImage(image, 0, 0, canvas.width, canvas.height);
    result.stage = 'painted';
    try {
      result.opening = [...context.getImageData(192, 160, 1, 1).data];
      result.fill = [...context.getImageData(192, 80, 1, 1).data];
      result.corner = [...context.getImageData(0, 0, 1, 1).data];
    } catch (error) { result.readbackError = error.name; }
    try {
      const blob = await pngBlob(canvas);
      result.pngType = blob.type;
      result.pngBytes = blob.size;
      result.pngDataURL = await dataURL(blob);
      result.export = 'ok';
      const download = document.createElement('a');
      download.href = result.pngDataURL;
      download.download = test.id + '.png';
      download.textContent = 'Save PNG';
      figure.append(download);
    } catch (error) { result.export = 'failed'; result.exportError = error.name; }
    caption.textContent = test.id + ': ' + result.stage + ', PNG ' + result.export;
    return result;
  }
  async function run() {
    const tests = [
      { id: 'same-origin', src: 'master.svg' },
      { id: 'cors-allowed', src: 'http://127.0.0.1:8783/cors/master.svg', crossOrigin: 'anonymous' },
      { id: 'no-cors-image', src: 'http://127.0.0.1:8783/plain/master.svg' },
      { id: 'cors-denied', src: 'http://127.0.0.1:8783/plain/master.svg', crossOrigin: 'anonymous' },
      { id: 'missing-file', src: 'missing.svg' },
    ];
    const results = [];
    for (const test of tests) results.push(await render(test));
    document.getElementById('report').textContent = JSON.stringify({ pageOrigin: location.origin, results }, (key, value) => key === 'pngDataURL' ? undefined : value, 2);
  }
  run();
</script>
</html>

La fonction d’export gère à la fois une exception levée et un résultat blob nul. Canvas.toBlob() prend en charge le PNG et peut lever SecurityError lorsque le canvas n’est pas propre du point de vue de l’origine. Les deux cas réussis ajoutent un lien d’enregistrement PNG ; le rapport visible omet le contenu base64.

Lis l’étape d’échec avant de changer le SVG

Nous avons exécuté exactement cette base dans un environnement de navigateur Chrome, puis rouvert les deux PNG enregistrés. Ces observations concernent le fichier original autonome et ces deux origines locales.

CasRequête d’imageDessin/lecture/export observés
Même origineSVG local, sans attribut CORSDécodé à 240 × 160 ; peint ; lecture et PNG réussis
Autre origine, autorisation accordéeanonymous et en-tête de réponseDécodé ; peint ; lecture et PNG réussis
Autre origine, sans requête CORSSans attribut ni en-tête de réponseDécodé et peint ; lecture et export ont levé SecurityError
Autre origine, autorisation refuséeanonymous, sans en-tête de réponseDécodage rejeté avec EncodingError ; aucun dessin tenté
Fichier absent404 de même origineDécodage rejeté avec EncodingError ; aucun dessin tenté

Le cas sans CORS affichait visiblement le dessin. Cette apparence n’établissait pas l’autorisation de le lire ou de l’exporter. Le cas CORS refusé et le fichier absent échouaient plus tôt : réécrire la géométrie ne corrigerait donc aucun de ces échecs de requête.

Les deux PNG exportés se sont rouverts à 480 × 320. Dans cette exécution, ils étaient identiques octet pour octet, chacun de 10509 octets. À la coordonnée bitmap (192,160), l’ouverture était RGBA (0,0,0,0) ; le coin avait aussi un alpha nul. L’échantillon bleu-vert à (192,80) était (23,107,91,255). Ces échantillons vérifient l’ouverture et le fond de ce fichier, pas chaque pixel de bord ni chaque fonction SVG.

Les rectangles blancs dans la page du navigateur sont des fonds CSS derrière les canvas. Ce ne sont pas de la peinture blanche ajoutée au PNG. Si ton résultat nécessite un fond opaque, peins-le explicitement avant de dessiner l’image, puis vérifie à nouveau le fichier exporté.

Garde le maître modifiable séparé du PNG

Le PNG enregistré contient des pixels. Il ne transporte pas le chemin, le cercle ou le contour intérieur modifiable d’origine. Garde master.svg comme source vectorielle et choisis les dimensions bitmap du canvas pour la destination qui a réellement besoin d’un PNG. Agrandir ensuite un PNG existant ne restaure pas sa géométrie source.

Un nom de fichier SVG seul ne prouve pas non plus que tout le dessin à l’intérieur est vectoriel ; utilise le guide d’inspection des images matricielles intégrées lorsqu’un fichier contient des images liées ou intégrées.

Si ton dessin source n’existe qu’en image matricielle et que tu as besoin de contours modifiables, le processus d’image en vecteurs de PerfectVector peut préparer un maître SVG. Examine son contour, ses couleurs et ses ouvertures avant de l’utiliser dans cette routine de chargement. La vectorisation ne corrige pas les en-têtes CORS et n’accorde pas l’autorisation de lire le canvas.

Questions fréquentes

Puis-je fournir du balisage SVG directement à drawImage() ? Non. Cet exemple charge un fichier SVG dans un HTMLImageElement et fournit l’image décodée à drawImage(). Une chaîne de balisage n’est pas cet objet image. Distingue le chargement du document de la construction de données de chemin avec Path2D.

Pourquoi l’image apparaît-elle mais l’export PNG échoue-t-il ? Une image d’une autre origine chargée sans autorisation CORS peut contaminer le canvas une fois dessinée. Dans notre cas sans CORS, le dessin réussissait, mais la lecture des pixels et l’export levaient tous deux SecurityError. Vérifie le mode de requête et la réponse du serveur au lieu de supposer que la géométrie SVG est cassée.

crossOrigin anonymous corrige-t-il toutes les URL d’image ? Non. Définis-le avant src, et le serveur d’images doit fournir une réponse CORS compatible. Notre même dessin échouait au décodage lorsque anonymous était demandé à l’endpoint sans en-tête d’autorisation.

Exporter canvas en PNG conserve-t-il des chemins SVG modifiables ? Non. PNG est une sortie matricielle. Conserve le SVG original séparément si tu dois modifier des chemins, formes ou ouvertures, et examine le PNG à sa taille de livraison prévue.

Sources

  1. MDN : CanvasRenderingContext2D.drawImage() — Objets image acceptés, dimensionnement de destination et dimensions intrinsèques.
  2. MDN : HTMLImageElement.decode() — Promesse de disponibilité d’image et rejet de requête ou de décodage.
  3. MDN : Images autorisées par CORS — Restrictions de propreté d’origine sur la lecture et l’export d’images dessinées.
  4. MDN : HTMLImageElement.crossOrigin — Mode de requête CORS pour charger les images.
  5. MDN : HTMLCanvasElement.toBlob() — Génération de blobs PNG, sortie nulle et SecurityError.
  6. MDN : SVG comme image — Contextes d’image SVG et restrictions de ressources externes.

Tu pars d’un original matriciel et as besoin d’un dessin modifiable ? Crée un maître SVG avec PerfectVector, examine son contour et ses ouvertures, puis garde ce maître pendant que tu vérifies séparément le rendu du canvas et le PNG exporté.

Plus depuis le blog

PerfectVector

Repartez d’un SVG plus net
et plus facile à modifier

Sans carte bancaire