PerfectVector
Von Irene Kim

SVG auf Canvas: Grafik vor dem PNG-Export laden

Lade ein vollständiges SVG mit Image.decode, zeichne es in gewählter Bitmapgröße, prüfe CORS und PNG-Auslesung und halte die bearbeitbare Quelle von exportierten Pixeln getrennt.

9 Min. Lesezeit
Auf dieser Seite

Um ein vollständiges SVG auf Canvas zu zeichnen, lade es als Bild, warte auf image.decode() und übergib dieses Bild an drawImage(). Prüfe anschließend den PNG-Export separat. Ein Bild kann auf Canvas erscheinen, während der Browser das Lesen oder Speichern seiner Pixel blockiert.

Dieser Weg zeichnet das SVG-Dokument als Bild. Er gibt dem Canvas keine bearbeitbaren SVG-Pfade. Möchtest du stattdessen ausgewählte Konturen rekonstruieren, verwende den Path2D-SVG-Leitfaden. Behalte das ursprüngliche SVG unabhängig vom gewählten Darstellungsweg.

Trenne Dekodierung, Zeichnen und Export

Ein HTMLImageElement ist eine akzeptierte Eingabe für Canvas-drawImage(). Seine Zielbreite und -höhe steuern die gezeichnete Größe. Image.decode() gibt ein Promise zurück, das nach dem Dekodieren aufgelöst wird. Ein fehlgeschlagener Abruf oder ein Dekodierungsproblem kann es zurückweisen.

Erfolgreiches Dekodieren ist nur die erste Prüfung. Ein Bild einer anderen Herkunft, das ohne CORS-Erlaubnis geladen wurde, kann weiterhin gezeichnet werden, verunreinigt aber das Canvas. Pixelauslesung und Export scheitern dann. MDNs Leitfaden zu CORS-freigegebenen Bildern erklärt diesen Unterschied.

Quelle behalten, Pixel prüfen
Schematische Illustration einer hohlen Raute und eines Kreises als Quellkonturen, glatt gerenderte Grafik und Pixelraster-Ausgabe
Illustration: Quellkonturen und gezeichnete Pixel sind verschiedene Repräsentationen. Rahmen, Pfeile und grobe Raster sind schematisch. Die folgenden Browserergebnisse stammen aus einer unabhängig ausgeführten selbst erstellten Testdatei.

Erstelle ein eigenständiges SVG

Speichere dies als master.svg. Es enthält einen Pfad und einen Kreis, ohne eingebettetes Bild, Schrift oder externe Abhängigkeit. Die innere Raute und die umgebende Zeichenfläche sind in der Quelle transparent.

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

Wir verwenden die vollständige Datei als Bild, einschließlich beider Formen und ihrer viewBox. Anders als beim Extrahieren nur des d-Werts eines Pfads bitten wir den SVG-Bildrenderer des Browsers, das Dokument zu zeichnen.

Das Beispiel ist bewusst eigenständig. Für SVG als Bild gelten Einschränkungen, unter anderem für externe Ressourcen und Skripte. Siehe MDNs Leitfaden zu SVG als Bild. Betrachte das erfolgreiche Dekodieren dieser kleinen Datei nicht als Beleg dafür, dass ein beliebiges SVG externe Schriften, Stile oder verknüpfte Grafiken lädt. Vergleiche für die Ausgabe Inline-SVG und Bildelemente.

Betreibe zwei lokale Herkünfte mit unterschiedlichen CORS-Antworten

Zu einer Herkunft gehört der Port. Der folgende rein lokale Server stellt dieselben SVG-Bytes über zwei Herkünfte bereit. An der zweiten Herkunft enthält /cors/master.svg den Header Access-Control-Allow-Origin: *; /plain/master.svg nicht. Das isoliert die Antwortberechtigung von der Grafik.

Speichere dies als serve-proof.py neben dem SVG und dem folgenden HTML. Führe python3 serve-proof.py aus und öffne dann http://127.0.0.1:8770/starter.html. Beide Ports müssen frei sein. Änderst du sie, aktualisiere Server und HTML gemeinsam. Drücke im Terminal die Eingabetaste, um beide Server zu stoppen.

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()

Dieser freizügige Header gehört zu einer lokalen Testumgebung, die unsere eigene Grafik bereitstellt. Konfiguriere für einen produktiven Bildhost die passende erlaubte Herkunft auf einem von dir kontrollierten Server. Ein Bildattribut zu setzen gewährt keinen Zugriff auf Inhalte eines anderen Servers.

Dekodiere das Bild vor dem Zeichnen

Speichere diese vollständige Seite als starter.html. Jeder Fall bekommt ein eigenes Canvas. Der verunreinigte Fall kann deshalb die erfolgreichen Fälle nicht beeinträchtigen. Setze bei CORS-Anfragen crossOrigin vor src, wie MDNs crossOrigin-Referenz beschreibt.

Die Canvas-Bitmap ist ausdrücklich 480 × 320 groß; ihre CSS-Darstellungsgröße beträgt 240 × 160. Die intrinsischen Abmessungen des ursprünglichen Bilds sind 240 × 160. drawImage() skaliert es auf die gewählten Bitmapabmessungen. Das sind gewählte Abmessungen, keine Messung des Geräte-Pixelverhältnisses.

<!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>

Die Exportfunktion behandelt sowohl eine geworfene Ausnahme als auch ein null-Blob-Ergebnis. Canvas.toBlob() unterstützt PNG-Ausgabe und kann SecurityError werfen, wenn das Canvas nicht herkunftsrein ist. Die beiden erfolgreichen Fälle ergänzen einen Link zum Speichern des PNGs. Der sichtbare Bericht lässt die Base64-Nutzlast weg.

Lies die Fehlerstufe, bevor du das SVG änderst

Wir führten genau diese Vorlage in einer Chrome-Browserumgebung aus und öffneten anschließend beide gespeicherten PNGs erneut. Diese Beobachtungen betreffen die ursprüngliche eigenständige Datei und diese beiden lokalen Herkünfte.

FallBildanfrageBeobachtetes Zeichnen/Auslesen/Exportieren
Gleiche HerkunftLokales SVG, kein CORS-AttributMit 240 × 160 dekodiert; gezeichnet; Auslesung und PNG erfolgreich
Andere Herkunft, Erlaubnis vorhandenanonymous plus AntwortheaderDekodiert; gezeichnet; Auslesung und PNG erfolgreich
Andere Herkunft, keine CORS-AnfrageKein Attribut, kein AntwortheaderDekodiert und gezeichnet; Auslesung und Export warfen SecurityError
Andere Herkunft, Erlaubnis verweigertanonymous, kein AntwortheaderDekodierung mit EncodingError zurückgewiesen; kein Zeichenversuch
Fehlende Datei404 derselben HerkunftDekodierung mit EncodingError zurückgewiesen; kein Zeichenversuch

Der Fall ohne CORS zeigte die Grafik sichtbar an. Dieses Aussehen belegte keine Erlaubnis zum Lesen oder Exportieren. Der Fall mit verweigertem CORS und die fehlende Datei scheiterten früher. Eine neue Geometrie würde daher keinen dieser Anfragefehler beheben.

Beide exportierten PNGs ließen sich mit 480 × 320 erneut öffnen. In diesem Lauf waren sie byteidentisch und jeweils 10509 Bytes groß. An der Bitmapkoordinate (192,160) war die Öffnung RGBA (0,0,0,0). Auch die Ecke hatte Alpha null. Die blaugrüne Probe bei (192,80) war (23,107,91,255). Diese Proben prüfen Öffnung und Füllung dieser Datei, nicht jeden Kantenpixel oder jede SVG-Funktion.

Die weißen Rechtecke auf der Browserseite sind CSS-Hintergründe hinter den Canvas-Elementen. Sie sind keine dem PNG hinzugefügte weiße Farbe. Braucht dein Ergebnis einen undurchsichtigen Hintergrund, zeichne ihn ausdrücklich vor dem Bild und prüfe die exportierte Datei erneut.

Halte bearbeitbare Masterdatei und PNG getrennt

Das gespeicherte PNG enthält Pixel. Es enthält nicht den ursprünglichen Pfad, Kreis oder bearbeitbaren Innenumriss. Behalte master.svg als Vektorquelle und wähle die Canvas-Bitmapabmessungen für das Ziel, das tatsächlich ein PNG braucht. Ein vorhandenes PNG später zu vergrößern stellt seine Quellgeometrie nicht wieder her.

Ein SVG-Dateiname allein beweist auch nicht, dass alle enthaltene Grafik Vektorgeometrie ist. Nutze den Leitfaden zur Prüfung eingebetteter Rasterbilder, wenn eine Datei verknüpfte oder eingebettete Bilder enthält.

Existiert deine Quellgrafik nur als Rasterbild und brauchst du bearbeitbare Konturen, kann PerfectVectors Bild-zu-Vektor-Workflow eine SVG-Masterdatei vorbereiten. Prüfe Umriss, Farben und Öffnungen, bevor du sie in diesem Ladeablauf verwendest. Vektorisierung repariert keine CORS-Header und gewährt keine Canvas-Ausleseberechtigung.

Häufige Fragen

Kann ich SVG-Markup direkt an drawImage() übergeben? Nein. Dieses Beispiel lädt eine SVG-Datei in ein HTMLImageElement und übergibt das dekodierte Bild an drawImage(). Eine Markup-Zeichenfolge ist nicht dieses Bildobjekt. Halte Dokumentladen und Path2D-Konstruktion aus Pfaddaten getrennt.

Warum erscheint das Bild, aber der PNG-Export scheitert? Ein Bild einer anderen Herkunft, das ohne CORS-Erlaubnis geladen wird, kann das Canvas nach dem Zeichnen verunreinigen. In unserem Fall ohne CORS gelang das Zeichnen, aber sowohl Pixelauslesung als auch Export warfen SecurityError. Prüfe Anfragemodus und Serverantwort, statt eine beschädigte SVG-Geometrie anzunehmen.

Repariert crossOrigin anonymous jede Bild-URL? Nein. Setze es vor src. Der Bildserver muss eine kompatible CORS-Antwort liefern. Dieselbe Grafik scheiterte beim Dekodieren, als anonymous am Endpunkt ohne Erlaubnisheader angefragt wurde.

Erhält der Canvas-Export als PNG bearbeitbare SVG-Pfade? Nein. PNG ist Rasterausgabe. Behalte das ursprüngliche SVG separat, wenn du Pfade, Formen oder Öffnungen bearbeiten musst, und prüfe das PNG bei seiner vorgesehenen Ausgabegröße.

Quellen

  1. MDN: CanvasRenderingContext2D.drawImage() – Akzeptierte Bildobjekte, Zielgrößen und intrinsische Abmessungen.
  2. MDN: HTMLImageElement.decode() – Promise zur Bildbereitschaft und Zurückweisung bei Abruf-/Dekodierungsfehlern.
  3. MDN: CORS-freigegebene Bilder – Herkunftsreinheitsbeschränkungen beim Lesen und Exportieren gezeichneter Bilder.
  4. MDN: HTMLImageElement.crossOrigin – CORS-Anfragemodus beim Laden von Bildern.
  5. MDN: HTMLCanvasElement.toBlob() – PNG-Blob-Erzeugung, null-Ausgabe und SecurityError.
  6. MDN: SVG als Bild – SVG-Bildkontexte und Beschränkungen für externe Ressourcen.

Du arbeitest mit einem Rasteroriginal und brauchst bearbeitbare Grafik? Erstelle mit PerfectVector eine SVG-Masterdatei, prüfe Umriss und Öffnungen und behalte diese Masterdatei, während du Canvas-Darstellung und exportiertes PNG separat prüfst.

Mehr aus dem Blog

PerfectVector

Starte mit einer sauberen SVG,
die leichter zu bearbeiten ist

Keine Kreditkarte nötig