PerfectVector
Por Irene Kim

SVG para Canvas: carregue a arte antes de exportar PNG

Carregue um SVG inteiro com Image.decode, desenhe no tamanho de bitmap escolhido, teste CORS e leitura PNG e separe a origem editável dos pixels exportados.

10 min de leitura
Nesta página

Para desenhar um SVG inteiro em canvas, carregue-o como imagem, aguarde image.decode() e passe a imagem para drawImage(). Depois confira a exportação PNG separadamente: uma imagem pode aparecer no canvas enquanto o navegador bloqueia a leitura ou o salvamento de seus pixels.

Essa rota pinta o documento SVG como uma imagem. Não fornece ao canvas caminhos SVG editáveis. Se você quer reconstruir contornos selecionados, use o guia de SVG com Path2D. Mantenha o SVG original, qualquer que seja a rota de exibição escolhida.

Separe decodificação, pintura e exportação

Um HTMLImageElement é uma entrada aceita por canvas drawImage(). Sua largura e altura de destino controlam o tamanho pintado. Image.decode() retorna uma promise resolvida quando a imagem é decodificada; uma requisição malsucedida ou problema de decodificação pode rejeitá-la.

Decodificação bem-sucedida é só a primeira verificação. Uma imagem de outra origem carregada sem permissão CORS ainda pode ser desenhada, mas contamina o canvas. A leitura de pixels e a exportação então falham. O guia de imagens com CORS da MDN explica a distinção.

Mantenha a origem, confira os pixels
Ilustração conceitual de um losango vazado e círculo como contornos de origem, arte renderizada suave e saída em grade de pixels
Ilustração: contornos de origem e pixels pintados são representações diferentes. Quadros, setas e grades grossas são conceituais; os resultados do navegador abaixo vêm de um arquivo original executado de forma independente.

Crie um SVG original independente

Salve como master.svg. Ele contém um caminho e um círculo, sem imagem incorporada, fonte ou dependência externa. O losango interno e a tela ao redor são transparentes na origem.

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

Usamos o arquivo completo como imagem, incluindo as duas formas e o viewBox. Ao contrário de extrair apenas o valor d de um caminho, isso pede ao renderizador de imagens SVG do navegador para pintar o documento.

O exemplo é intencionalmente independente. SVG usado como imagem tem restrições, inclusive a recursos externos e scripts; veja o guia da MDN sobre SVG como imagem. Não trate a decodificação bem-sucedida deste arquivo pequeno como prova de que qualquer SVG carregará fontes, estilos ou arte vinculada externos. Para escolhas de entrega, compare SVG inline com elemento de imagem.

Execute duas origens locais com respostas CORS diferentes

Uma origem inclui sua porta. O servidor apenas local abaixo expõe os mesmos bytes SVG por duas origens. Na segunda, /cors/master.svg inclui Access-Control-Allow-Origin: *; /plain/master.svg não. Isso isola a permissão da resposta da arte.

Salve como serve-proof.py junto do SVG e do HTML abaixo. Execute python3 serve-proof.py e abra http://127.0.0.1:8770/starter.html. As duas portas precisam estar livres; se mudá-las, atualize o servidor e o HTML juntos. Pressione Enter no terminal para parar os dois servidores.

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

Esse cabeçalho permissivo pertence a um arquivo de teste local que serve nossa própria arte. Para um host de imagens em produção, configure a origem permitida adequada em um servidor que você controla. Definir um atributo de imagem não concede acesso ao conteúdo de outro servidor.

Decodifique a imagem antes de desenhá-la

Salve esta página completa como starter.html. Cada caso recebe seu próprio canvas, assim o caso contaminado não afeta os bem-sucedidos. Defina crossOrigin antes de src nas requisições que precisam de CORS, como descreve a referência de crossOrigin da MDN.

O bitmap do canvas é explicitamente 480 × 320; seu tamanho de exibição CSS é 240 × 160. As dimensões intrínsecas da imagem original são 240 × 160, e drawImage() a escala para as dimensões de bitmap selecionadas. São dimensões escolhidas, não uma medida da razão de pixels do dispositivo.

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

A função de exportação trata uma exceção lançada e um resultado blob nulo. Canvas.toBlob() suporta saída PNG e pode lançar SecurityError quando o canvas não está limpo em relação à origem. Os dois casos bem-sucedidos adicionam um link Save PNG; o relatório visível omite o conteúdo base64.

Leia a etapa de falha antes de mudar o SVG

Executamos exatamente esse exemplo em um ambiente de navegador Chrome e reabrimos os dois PNGs salvos. Essas observações dizem respeito ao arquivo original independente e às duas origens locais.

CasoRequisição de imagemDesenho/leitura/exportação observados
Mesma origemSVG local, sem atributo CORSDecodificado a 240 × 160; pintado; leitura e PNG bem-sucedidos
Outra origem, permissão concedidaanonymous mais cabeçalho de respostaDecodificado; pintado; leitura e PNG bem-sucedidos
Outra origem, sem requisição CORSSem atributo e sem cabeçalho de respostaDecodificado e pintado; leitura e exportação lançaram SecurityError
Outra origem, permissão negadaanonymous, sem cabeçalho de respostaDecodificação rejeitada com EncodingError; nenhum desenho foi tentado
Arquivo ausente404 na mesma origemDecodificação rejeitada com EncodingError; nenhum desenho foi tentado

O caso sem CORS mostrava a arte visivelmente. Essa aparência não estabelecia permissão para ler ou exportar. Os casos de CORS negado e arquivo ausente falharam antes, portanto reescrever a geometria não resolveria nenhuma dessas falhas de requisição.

Os dois PNGs exportados reabriram a 480 × 320. Nesta execução eram idênticos em bytes, com 10509 bytes cada. Na coordenada de bitmap (192,160), a abertura era RGBA (0,0,0,0); o canto também tinha alfa zero. A amostra verde-petróleo em (192,80) era (23,107,91,255). Essas amostras verificam a abertura e o preenchimento deste arquivo, não todo pixel de borda ou recurso SVG.

Os retângulos brancos na página do navegador são fundos CSS atrás dos canvases. Não são tinta branca adicionada ao PNG. Se seu resultado precisa de fundo opaco, pinte esse fundo explicitamente antes de desenhar a imagem e verifique o arquivo exportado novamente.

Mantenha o original editável separado do PNG

O PNG salvo contém pixels. Não carrega o caminho, círculo ou contorno interno editável originais. Mantenha master.svg como origem vetorial e escolha as dimensões do bitmap do canvas para o destino que realmente precisa de PNG. Ampliar um PNG existente depois não restaura a geometria de origem.

Um nome de arquivo SVG também não prova que toda a arte interna é geometria vetorial; use o guia de inspeção de raster incorporado quando um arquivo contém imagens vinculadas ou incorporadas.

Se sua arte de origem existe só como imagem raster e você precisa de contornos editáveis, o fluxo de imagem para vetor do PerfectVector pode preparar um original SVG. Examine contorno, cores e aberturas antes de usá-lo nesta rotina de carregamento. A vetorização não corrige cabeçalhos CORS nem concede permissão de leitura do canvas.

Perguntas frequentes

Posso passar marcação SVG diretamente para drawImage()? Não. Este exemplo carrega um arquivo SVG em um HTMLImageElement e passa a imagem decodificada para drawImage(). Uma string de marcação não é esse objeto de imagem. Separe o carregamento do documento da construção de dados de caminhos Path2D.

Por que a imagem aparece, mas a exportação PNG falha? Uma imagem de outra origem carregada sem permissão CORS pode contaminar o canvas depois de desenhada. No nosso caso sem CORS, a pintura funcionou, mas a leitura de pixels e a exportação lançaram SecurityError. Confira o modo da requisição e a resposta do servidor em vez de supor que a geometria SVG está quebrada.

crossOrigin anonymous corrige qualquer URL de imagem? Não. Defina antes de src, e o servidor da imagem precisa fornecer uma resposta CORS compatível. Nossa mesma arte falhou na decodificação quando anonymous foi solicitado ao endpoint sem cabeçalho de permissão.

Exportar canvas como PNG preserva caminhos SVG editáveis? Não. PNG é saída raster. Mantenha o SVG original separado se precisar editar caminhos, formas ou aberturas e examine o PNG no tamanho de entrega pretendido.

Fontes

  1. MDN: CanvasRenderingContext2D.drawImage() — Objetos de imagem aceitos, tamanho de destino e dimensões intrínsecas.
  2. MDN: HTMLImageElement.decode() — Promise de prontidão da imagem e rejeição de requisição/decodificação.
  3. MDN: Imagens com CORS — Restrições de origem limpa para leitura e exportação de imagens desenhadas.
  4. MDN: HTMLImageElement.crossOrigin — Modo de requisição CORS para carregar imagens.
  5. MDN: HTMLCanvasElement.toBlob() — Geração de blob PNG, saída nula e SecurityError.
  6. MDN: SVG como imagem — Contextos de imagem SVG e restrições de recursos externos.

Está trabalhando com um original raster que precisa virar arte editável? Crie um original SVG com o PerfectVector, examine contorno e aberturas e mantenha esse original enquanto verifica separadamente a renderização no canvas e o PNG exportado.

Mais do blog

PerfectVector

Recomece com um SVG mais limpo
e fácil de editar

Sem cartão de crédito