PerfectVector
Autor: Irene Kim

SVG na Canvas: wczytaj grafikę przed eksportem PNG

Wczytaj całe SVG przez Image.decode, narysuj w wybranym rozmiarze bitmapy, sprawdź CORS i odczyt PNG oraz oddziel edytowalne źródło od eksportowanych pikseli.

9 min czytania
Na tej stronie

Aby narysować całe SVG na canvas, wczytaj je jako obraz, poczekaj na image.decode() i przekaż obraz do drawImage(). Potem osobno sprawdź eksport PNG: obraz może pojawić się na canvas, choć przeglądarka blokuje odczyt lub zapis jego pikseli.

Ta metoda maluje dokument SVG jako obraz. Nie udostępnia canvas edytowalnych ścieżek SVG. Jeśli chcesz odtworzyć wybrane kontury, użyj poradnika SVG Path2D. Zachowaj oryginalny SVG niezależnie od wybranej metody wyświetlania.

Oddziel dekodowanie, malowanie i eksportowanie

HTMLImageElement jest akceptowanym wejściem canvas drawImage(). Docelowa szerokość i wysokość kontrolują rozmiar malowania. Image.decode() zwraca promise spełnianą po zdekodowaniu obrazu; błędne żądanie lub problem dekodowania może ją odrzucić.

Udane dekodowanie to tylko pierwsza kontrola. Obraz z innego źródła wczytany bez uprawnienia CORS nadal może zostać narysowany, ale zanieczyszcza canvas. Odczyt pikseli i eksport wtedy zawodzą. Poradnik obrazów z CORS MDN wyjaśnia różnicę.

Zachowaj źródło, sprawdź piksele
Schemat koncepcyjny pustego rombu i koła jako konturów źródłowych, gładkiej renderowanej grafiki i wyniku w siatce pikseli
Ilustracja: kontury źródłowe i namalowane piksele są różnymi reprezentacjami. Ramki, strzałki i zgrubne siatki są koncepcyjne; wyniki przeglądarkowe poniżej pochodzą z niezależnie wykonanego oryginalnego testu.

Zbuduj samowystarczalny oryginalny SVG

Zapisz go jako master.svg. Zawiera jedną ścieżkę i jedno koło, bez osadzonego obrazu, fontu lub zewnętrznej zależności. Wewnętrzny romb i otaczający obszar są przezroczyste w źródle.

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

Używamy całego pliku jako obrazu, razem z oboma kształtami i viewBox. W przeciwieństwie do wyodrębnienia samego d ścieżki prosimy renderer obrazów SVG przeglądarki o namalowanie dokumentu.

Przykład jest celowo samowystarczalny. SVG używany jako obraz ma ograniczenia, między innymi zasobów zewnętrznych i skryptów; zobacz poradnik SVG jako obraz MDN. Nie traktuj udanego dekodowania małego pliku jako dowodu, że dowolny SVG wczyta zewnętrzne fonty, style lub powiązaną grafikę. Wybierając sposób przekazania, porównaj SVG inline z elementem obrazu.

Uruchom dwa lokalne źródła z różnymi odpowiedziami CORS

Źródło obejmuje port. Poniższy wyłącznie lokalny serwer udostępnia te same bajty SVG przez dwa źródła. W drugim /cors/master.svg zawiera Access-Control-Allow-Origin: *, a /plain/master.svg nie. Izoluje to uprawnienie odpowiedzi od grafiki.

Zapisz kod jako serve-proof.py obok SVG i HTML poniżej. Uruchom python3 serve-proof.py, a potem otwórz http://127.0.0.1:8770/starter.html. Oba porty muszą być wolne; jeśli je zmienisz, zaktualizuj serwer i HTML razem. Naciśnij Enter w terminalu, aby zatrzymać oba serwery.

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

Ten liberalny nagłówek należy do lokalnego testu udostępniającego naszą własną grafikę. Dla produkcyjnego hosta obrazów skonfiguruj odpowiednie dozwolone źródło na serwerze, który kontrolujesz. Ustawienie atrybutu obrazu nie daje dostępu do zawartości innego serwera.

Zdekoduj obraz przed rysowaniem

Zapisz kompletną stronę jako starter.html. Każdy przypadek ma własny canvas, więc zanieczyszczony przypadek nie wpływa na udane. Ustaw crossOrigin przed src dla żądań wymagających CORS, zgodnie z opisem crossOrigin MDN.

Bitmapa canvas ma jawnie 480 × 320, a rozmiar wyświetlania CSS 240 × 160. Wewnętrzne wymiary oryginalnego obrazu to 240 × 160, a drawImage() skaluje go do wybranej bitmapy. To wybrane wymiary, a nie pomiar współczynnika pikseli urządzenia.

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

Funkcja eksportu obsługuje zarówno zgłoszony wyjątek, jak i wynik blob równy null. Canvas.toBlob() obsługuje PNG i może zgłosić SecurityError, gdy canvas nie jest czysty względem źródła. Dwa udane przypadki dodają odnośnik Save PNG; widoczny raport pomija dane base64.

Odczytaj etap błędu przed zmianą SVG

Wykonaliśmy dokładnie ten przykład w jednym środowisku Chrome, a potem ponownie otworzyliśmy oba zapisane PNG. Obserwacje dotyczą oryginalnego samowystarczalnego pliku i tych dwóch lokalnych źródeł.

PrzypadekŻądanie obrazuZaobserwowane rysowanie/odczyt/eksport
To samo źródłoLokalny SVG, brak atrybutu CORSZdekodowano w 240 × 160; namalowano; odczyt i PNG udane
Inne źródło, uprawnienie przyznaneanonymous i nagłówek odpowiedziZdekodowano; namalowano; odczyt i PNG udane
Inne źródło, brak żądania CORSBrak atrybutu i nagłówka odpowiedziZdekodowano i namalowano; odczyt i eksport zgłosiły SecurityError
Inne źródło, uprawnienie odrzuconeanonymous, brak nagłówka odpowiedziDekodowanie odrzucono z EncodingError; nie próbowano rysować
Brakujący plik404 z tego samego źródłaDekodowanie odrzucono z EncodingError; nie próbowano rysować

Przypadek bez CORS widocznie pokazywał grafikę. Wygląd nie oznaczał prawa do jej odczytu lub eksportu. Przypadek odrzuconego CORS i brakującego pliku zawiodły wcześniej, więc przepisanie geometrii nie naprawiłoby żadnego błędu żądania.

Oba eksportowane PNG otworzyły się ponownie jako 480 × 320. W tym wykonaniu były identyczne bajtowo, każdy miał 10509 bajtów. We współrzędnej bitmapy (192,160) otwór miał RGBA (0,0,0,0); narożnik także miał alfa zero. Morskozielona próbka w (192,80) wynosiła (23,107,91,255). Próbki sprawdzają otwór i wypełnienie tego pliku, nie każdy piksel krawędzi ani każdą funkcję SVG.

Białe prostokąty na stronie przeglądarki są tłami CSS za canvas. Nie są białym malowaniem dodanym do PNG. Jeśli wynik wymaga nieprzezroczystego tła, namaluj je jawnie przed narysowaniem obrazu i ponownie sprawdź eksportowany plik.

Zachowaj edytowalny wzorzec oddzielnie od PNG

Zapisany PNG zawiera piksele. Nie przenosi oryginalnej ścieżki, koła ani edytowalnego konturu wewnętrznego. Zachowaj master.svg jako wektorowe źródło i wybierz wymiary bitmapy canvas dla miejsca docelowego, które faktycznie potrzebuje PNG. Późniejsze powiększanie istniejącego PNG nie odtwarza geometrii źródła.

Sama nazwa SVG także nie dowodzi, że cała grafika wewnątrz jest wektorowa; użyj poradnika kontroli osadzonego rastra, gdy plik zawiera powiązane lub osadzone obrazy.

Jeśli grafika źródłowa istnieje tylko jako raster i potrzebujesz edytowalnych konturów, procedura zamiany obrazu na wektor PerfectVector może przygotować wzorzec SVG. Sprawdź kontur, kolory i otwory przed użyciem go w tej procedurze wczytywania. Wektoryzacja nie naprawia nagłówków CORS ani nie daje prawa do odczytu canvas.

Najczęstsze pytania

Czy mogę przekazać kod SVG bezpośrednio do drawImage()? Nie. Przykład wczytuje plik SVG do HTMLImageElement i przekazuje zdekodowany obraz do drawImage(). Ciąg znaczników nie jest tym obiektem obrazu. Oddziel wczytywanie dokumentu od budowania danych ścieżek Path2D.

Dlaczego obraz jest widoczny, ale eksport PNG nie działa? Obraz z innego źródła wczytany bez uprawnień CORS może zanieczyścić canvas po narysowaniu. W naszym przypadku bez CORS malowanie się udało, ale odczyt pikseli i eksport zgłosiły SecurityError. Sprawdź tryb żądania i odpowiedź serwera, zamiast zakładać uszkodzenie geometrii SVG.

Czy crossOrigin anonymous naprawia każdy adres obrazu? Nie. Ustaw go przed src, a serwer obrazu musi dostarczyć zgodną odpowiedź CORS. Ta sama grafika nie przeszła dekodowania, gdy żądano anonymous z punktu bez nagłówka uprawnienia.

Czy eksport canvas do PNG zachowuje edytowalne ścieżki SVG? Nie. PNG jest wynikiem rastrowym. Zachowaj oryginalny SVG oddzielnie do edycji ścieżek, kształtów lub otworów i sprawdź PNG w docelowym rozmiarze przekazania.

Źródła

  1. MDN: CanvasRenderingContext2D.drawImage() — Akceptowane obiekty obrazu, wymiary docelowe i wewnętrzne.
  2. MDN: HTMLImageElement.decode() — Promise gotowości obrazu i odrzucanie żądania/dekodowania.
  3. MDN: Obrazy z CORS — Ograniczenia czystości źródła przy odczycie i eksporcie narysowanych obrazów.
  4. MDN: HTMLImageElement.crossOrigin — Tryb żądania CORS przy wczytywaniu obrazu.
  5. MDN: HTMLCanvasElement.toBlob() — Generowanie blob PNG, wynik null i SecurityError.
  6. MDN: SVG jako obraz — Konteksty obrazu SVG i ograniczenia zasobów zewnętrznych.

Pracujesz z rastrowym oryginałem wymagającym edytowalnej grafiki? Utwórz wzorzec SVG przez PerfectVector, sprawdź kontur i otwory, a potem zachowaj wzorzec, kontrolując osobno renderowanie canvas oraz eksportowany PNG.

Więcej z bloga

PerfectVector

Zacznij od czystszego SVG,
który łatwiej edytować

Bez karty kredytowej