PerfectVector
Yazan Irene Kim

SVG'den Canvas'a: PNG Çıktısından Önce Çizimi Yükleyin

Tam SVG'yi Image.decode ile yükleyin, seçilen bitmap boyutunda çizin, CORS ve PNG geri okumasını sınayın, düzenlenebilir kaynağı çıktı piksellerinden ayrı tutun.

9 dk okuma
Bu sayfada

Tam SVG'yi canvas'a çizmek için görsel olarak yükleyin, image.decode() işlemini bekleyin ve görseli drawImage() işlevine verin. Sonra PNG dışa aktarımını ayrı kontrol edin: tarayıcı pikselleri okumayı veya kaydetmeyi engellerken görsel canvas'ta görünebilir.

Bu yol SVG belgesini görsel olarak boyar. Canvas'a düzenlenebilir SVG yolları vermez. Bunun yerine seçilen konturları yeniden oluşturmak istiyorsanız Path2D SVG kılavuzunu kullanın. Hangi görüntüleme yolunu seçerseniz seçin özgün SVG'yi koruyun.

Çözme, boyama ve dışa aktarmayı ayırın

HTMLImageElement, canvas drawImage() için kabul edilen girdidir. Hedef genişlik ve yükseklik boyanan boyutu yönetir. Image.decode(), görsel çözüldüğünde tamamlanan promise döndürür; başarısız istek veya çözme sorunu bunu reddedebilir.

Başarılı çözme yalnızca ilk kontroldür. CORS izni olmadan yüklenen kökenler arası görsel yine çizilebilir, ancak canvas'ı kirletir. Piksel geri okuma ve dışa aktarma sonra başarısız olur. MDN'nin CORS etkin görsel kılavuzu bu ayrımı açıklar.

Kaynağı koruyun, pikselleri kontrol edin
Delikli eşkenar dörtgen ve dairenin kaynak konturları, pürüzsüz işlenmiş çizim ve piksel ızgaralı çıktı olarak kavramsal illüstrasyonu
İllüstrasyon: kaynak konturları ve boyanmış pikseller farklı temsillerdir. Çerçeveler, oklar ve kaba ızgaralar kavramsaldır; aşağıdaki tarayıcı sonuçları bağımsız çalıştırılmış özgün testten gelir.

Bağımsız bir özgün SVG oluşturun

Bunu master.svg olarak kaydedin. Gömülü görsel, yazı tipi veya harici bağımlılık olmadan bir yol ve bir daire içerir. İç eşkenar dörtgen ve çevresindeki tuval kaynakta saydamdır.

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

İki şekli ve viewBox değerini içeren tam dosyayı görsel olarak kullanıyoruz. Yalnızca yolun d değerini çıkarmaktan farklı olarak tarayıcının SVG görsel işleyicisinden belgeyi boyamasını ister.

Örnek bilinçli olarak bağımsızdır. Görsel olarak kullanılan SVG'nin harici kaynaklar ve betikler dâhil kısıtlamaları vardır; MDN'nin görsel olarak SVG kılavuzuna bakın. Bu küçük dosyanın başarıyla çözülmesini, rastgele SVG'nin harici yazı tiplerini, stilleri veya bağlı çizimleri yükleyeceğinin kanıtı saymayın. Teslim seçenekleri için satır içi SVG'yi görsel öğesiyle karşılaştırın.

Farklı CORS yanıtları olan iki yerel köken çalıştırın

Köken bağlantı noktasını içerir. Aşağıdaki yalnızca yerel sunucu aynı SVG baytlarını iki kökenden sunar. İkinci kökende /cors/master.svg, Access-Control-Allow-Origin: * içerir; /plain/master.svg içermez. Bu, yanıt iznini çizimden ayırır.

Bunu SVG ve aşağıdaki HTML'nin yanına serve-proof.py olarak kaydedin. python3 serve-proof.py çalıştırıp http://127.0.0.1:8770/starter.html açın. İki bağlantı noktası boş olmalıdır; değiştirirseniz sunucuyu ve HTML'yi birlikte güncelleyin. İki sunucuyu durdurmak için terminalde Enter'a basın.

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

Bu izin veren başlık, kendi çizimimizi sunan yerel teste aittir. Üretim görsel sunucusunda kontrol ettiğiniz sunucu için uygun izinli kökeni yapılandırın. Görsel özniteliği ayarlamak başka sunucunun içeriğine erişim izni vermez.

Çizmeden önce görseli çözün

Bu eksiksiz sayfayı starter.html olarak kaydedin. Her durumun kendi canvas'ı vardır; kirlenmiş durum başarılı olanları kirletemez. CORS gerektiren isteklerde MDN crossOrigin başvurusunun açıkladığı gibi crossOrigin değerini src öncesinde ayarlayın.

Canvas bitmap'i açıkça 480 × 320, CSS görüntüleme boyutu 240 × 160'tır. Özgün görselin içsel boyutları 240 × 160'tır ve drawImage() onu seçilen bitmap boyutlarına ölçekler. Bunlar cihaz piksel oranı ölçümü değil, seçilmiş boyutlardır.

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

Dışa aktarma işlevi hem fırlatılan istisnayı hem null blob sonucunu işler. Canvas.toBlob(), PNG çıktısını destekler ve canvas origin-clean değilse SecurityError fırlatabilir. İki başarılı durum PNG Kaydet bağlantısı ekler; görünür rapor base64 yükünü içermez.

SVG'yi değiştirmeden önce hata aşamasını okuyun

Bu başlangıcı tek Chrome tarayıcı ortamında aynen çalıştırdık, ardından iki kayıtlı PNG'yi tekrar açtık. Bu gözlemler özgün bağımsız dosya ve iki yerel kökenle ilgilidir.

DurumGörsel isteğiGözlenen çizim/geri okuma/dışa aktarma
Aynı kökenYerel SVG, CORS özniteliği yok240 × 160'ta çözüldü; boyandı; geri okuma ve PNG başarılı
Farklı köken, izin verildianonymous ve yanıt başlığıÇözüldü; boyandı; geri okuma ve PNG başarılı
Farklı köken, CORS isteği yokÖznitelik ve yanıt başlığı yokÇözüldü ve boyandı; geri okuma ve dışa aktarma SecurityError fırlattı
Farklı köken, izin reddedildianonymous, yanıt başlığı yokÇözme EncodingError ile reddedildi; çizim denenmedi
Eksik dosyaAynı köken 404Çözme EncodingError ile reddedildi; çizim denenmedi

CORS olmayan durum çizimi görünür biçimde gösterdi. Bu görünüm, okuma veya dışa aktarma iznini kanıtlamadı. CORS reddi ve eksik dosya durumları daha erken başarısız oldu; dolayısıyla geometriyi yeniden yazmak iki istek hatasını da çözmezdi.

Dışa aktarılan iki PNG de 480 × 320 boyutunda tekrar açıldı. Bu çalıştırmada baytları özdeşti; her biri 10.509 bayttı. Bitmap koordinatı (192,160) konumunda açıklık RGBA (0,0,0,0) idi; köşede de alfa sıfırdı. (192,80) konumundaki koyu turkuaz örnek (23,107,91,255) idi. Bunlar bu dosyanın açıklığını ve dolgusunu kontrol eder; her kenar pikselini veya SVG özelliğini değil.

Tarayıcı sayfasındaki beyaz dikdörtgenler canvas'ların arkasındaki CSS arka planlarıdır. PNG'ye eklenmiş beyaz boya değildir. Sonucunuz opak arka plan gerektiriyorsa görselden önce arka planı açıkça boyayın ve dışa aktarılan dosyayı yeniden doğrulayın.

Düzenlenebilir ana dosyayı PNG'den ayrı tutun

Kayıtlı PNG pikseller içerir. Özgün yolu, daireyi veya düzenlenebilir iç konturu taşımaz. master.svg dosyasını vektör kaynak olarak koruyun ve canvas bitmap boyutlarını gerçekten PNG isteyen hedefe göre seçin. Mevcut PNG'yi daha sonra büyütmek kaynak geometrisini geri getirmez.

Yalnızca SVG dosya adı, içerideki tüm çizimin vektör geometri olduğunu kanıtlamaz; dosya bağlı veya gömülü görseller içeriyorsa gömülü raster inceleme kılavuzunu kullanın.

Kaynak çiziminiz yalnızca raster görsel olarak mevcutsa ve düzenlenebilir konturlar gerekiyorsa PerfectVector görüntüden vektöre iş akışı SVG ana dosyası hazırlayabilir. Bu yükleme rutininde kullanmadan önce dış hattını, renklerini ve açıklıklarını inceleyin. Vektörleştirme CORS başlıklarını düzeltmez veya canvas geri okuma izni vermez.

Sık sorulan sorular

SVG işaretlemesini doğrudan drawImage() işlevine verebilir miyim? Hayır. Bu örnek SVG dosyasını HTMLImageElement içine yükler ve çözülen görseli drawImage() işlevine verir. İşaretleme dizgesi bu görsel nesnesi değildir. Belge yüklemeyi Path2D yol verisi oluşturmadan ayrı tutun.

Görsel neden görünüyor ama PNG çıktısı başarısız oluyor? CORS izni olmadan yüklenen kökenler arası görsel, çizildikten sonra canvas'ı kirletebilir. CORS olmayan durumumuzda boyama başarılıydı, ancak piksel geri okuma ve dışa aktarma SecurityError fırlattı. SVG geometrisinin bozuk olduğunu varsaymak yerine istek modunu ve sunucu yanıtını kontrol edin.

crossOrigin anonymous her görsel URL'sini düzeltir mi? Hayır. src öncesinde ayarlayın ve görsel sunucusu uyumlu CORS yanıtı sağlamalıdır. Aynı çizimimiz, izin başlığı olmayan uç noktadan anonymous istendiğinde çözülemedi.

Canvas'ı PNG olarak dışa aktarmak düzenlenebilir SVG yollarını korur mu? Hayır. PNG raster çıktıdır. Yolları, şekilleri veya açıklıkları düzenlemeniz gerekiyorsa özgün SVG'yi ayrı koruyun ve PNG'yi amaçlanan teslim boyutunda inceleyin.

Kaynaklar

  1. MDN: CanvasRenderingContext2D.drawImage() — Kabul edilen görsel nesneleri, hedef boyutlandırma ve içsel boyutlar.
  2. MDN: HTMLImageElement.decode() — Görsel hazır olma promise'i ve istek/çözme reddi.
  3. MDN: CORS etkin görseller — Çizilmiş görselleri okuma ve dışa aktarmada origin-clean kısıtlamaları.
  4. MDN: HTMLImageElement.crossOrigin — Görsel yükleme için CORS istek modu.
  5. MDN: HTMLCanvasElement.toBlob() — PNG blob üretimi, null çıktı ve SecurityError.
  6. MDN: Görsel olarak SVG — SVG görsel bağlamları ve harici kaynak kısıtlamaları.

Düzenlenebilir çizim gereken raster özgün dosyayla mı çalışıyorsunuz? PerfectVector ile SVG ana dosyası oluşturun, dış hattını ve açıklıklarını inceleyin, ardından canvas işlemesini ve dışa aktarılan PNG'yi ayrı kontrol ederken ana dosyayı koruyun.

Bloga daha fazlası

PerfectVector

Düzenlemesi daha kolay,
daha temiz bir SVG ile başlayın

Kredi kartı gerekmez