PerfectVector
Por Irene Kim

SVG a Canvas: carga el diseño antes de exportar PNG

Carga un SVG completo con Image.decode, dibújalo al tamaño de mapa de bits elegido, prueba CORS y la lectura de PNG, y separa el origen editable de los píxeles exportados.

10 min de lectura
En esta página

Para dibujar un SVG completo en canvas, cárgalo como imagen, espera a image.decode() y pasa esa imagen a drawImage(). Después comprueba la exportación PNG por separado: una imagen puede aparecer en canvas aunque el navegador bloquee la lectura o el guardado de sus píxeles.

Esta vía pinta el documento SVG como imagen. No proporciona al canvas trazados SVG editables. Si quieres reconstruir contornos seleccionados, usa la guía de SVG con Path2D. Conserva el SVG original sea cual sea la vía de visualización que elijas.

Separa decodificación, dibujo y exportación

Un HTMLImageElement es una entrada aceptada por drawImage() de canvas. Su ancho y alto de destino controlan el tamaño pintado. Image.decode() devuelve una promesa que se resuelve cuando la imagen está decodificada; una solicitud fallida o un problema de decodificación pueden rechazarla.

Una decodificación correcta es solo la primera comprobación. Una imagen de otro origen cargada sin permiso CORS todavía puede dibujarse, pero contamina el canvas. La lectura de píxeles y la exportación fallan entonces. La guía de imágenes con CORS de MDN explica esta diferencia.

Conserva el origen y revisa los píxeles
Ilustración conceptual de un rombo hueco y un círculo como contornos de origen, diseño renderizado suave y salida en cuadrícula de píxeles
Ilustración: los contornos de origen y los píxeles pintados son representaciones diferentes. Los marcos, las flechas y las cuadrículas gruesas son conceptuales; los resultados de navegador de abajo proceden de un archivo de prueba original ejecutado de forma independiente.

Crea un SVG original autónomo

Guarda esto como master.svg. Contiene un trazado y un círculo, sin imagen incrustada, fuente ni dependencia externa. El rombo interior y el lienzo circundante son transparentes en el origen.

<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 el archivo completo como imagen, incluidas ambas formas y su viewBox. A diferencia de extraer solo el valor d de un trazado, esto pide al renderizador de imágenes SVG del navegador que pinte el documento.

El ejemplo es autónomo de forma deliberada. Un SVG usado como imagen tiene restricciones, incluidas las de recursos externos y scripts; consulta la guía de SVG como imagen de MDN. No tomes una decodificación correcta de este archivo pequeño como prueba de que cualquier SVG cargará fuentes, estilos o diseños enlazados externos. Para elegir cómo entregarlo, compara SVG inline con un elemento de imagen.

Ejecuta dos orígenes locales con respuestas CORS distintas

Un origen incluye su puerto. El siguiente servidor solo local expone los mismos bytes SVG desde dos orígenes. En el segundo, /cors/master.svg incluye Access-Control-Allow-Origin: *; /plain/master.svg no. Así se aísla el permiso de respuesta del diseño.

Guarda esto como serve-proof.py junto al SVG y al HTML de abajo. Ejecuta python3 serve-proof.py y abre http://127.0.0.1:8770/starter.html. Ambos puertos deben estar libres; si los cambias, actualiza el servidor y el HTML juntos. Pulsa Intro en la terminal para detener ambos 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()

Este encabezado permisivo pertenece a un archivo de prueba local que sirve nuestro diseño. Para un servidor de imágenes de producción, configura el origen permitido adecuado en un servidor que controles. Establecer un atributo de imagen no da acceso al contenido de otro servidor.

Decodifica la imagen antes de dibujarla

Guarda esta página completa como starter.html. Cada caso recibe su propio canvas, así que el caso contaminado no puede contaminar los que funcionan. Establece crossOrigin antes de src para las solicitudes que necesiten CORS, como describe la referencia de crossOrigin de MDN.

El mapa de bits del canvas es explícitamente de 480 × 320; su tamaño de visualización CSS es de 240 × 160. Las dimensiones intrínsecas de la imagen original son 240 × 160, y drawImage() la escala a las dimensiones de mapa de bits elegidas. Son dimensiones seleccionadas, no una medición de la relación de píxeles del 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>

La función de exportación gestiona tanto una excepción lanzada como un resultado blob nulo. Canvas.toBlob() admite salida PNG y puede lanzar SecurityError cuando el canvas no está limpio respecto al origen. Los dos casos correctos añaden un enlace para guardar PNG; el informe visible omite los datos base64.

Lee la etapa del fallo antes de cambiar el SVG

Ejecutamos exactamente esta plantilla en un entorno de navegador Chrome y después volvimos a abrir ambos PNG guardados. Estas observaciones corresponden al archivo original autónomo y a estos dos orígenes locales.

CasoSolicitud de imagenDibujo, lectura y exportación observados
Mismo origenSVG local, sin atributo CORSDecodificado a 240 × 160; dibujado; lectura y PNG correctos
Otro origen, permiso concedidoanonymous y encabezado de respuestaDecodificado; dibujado; lectura y PNG correctos
Otro origen, sin solicitud CORSSin atributo ni encabezado de respuestaDecodificado y dibujado; lectura y exportación lanzaron SecurityError
Otro origen, permiso denegadoanonymous, sin encabezado de respuestaDecodificación rechazada con EncodingError; no se intentó dibujar
Archivo ausente404 del mismo origenDecodificación rechazada con EncodingError; no se intentó dibujar

El caso sin CORS mostró el diseño de forma visible. Esa apariencia no establecía permiso para leerlo o exportarlo. El caso con CORS denegado y el de archivo ausente fallaron antes, por lo que reescribir la geometría no resolvería ninguno de esos fallos de solicitud.

Ambos PNG exportados volvieron a abrirse a 480 × 320. En esta ejecución fueron idénticos byte a byte, con 10509 bytes cada uno. En la coordenada de mapa de bits (192,160), la abertura era RGBA (0,0,0,0); la esquina también tenía alfa cero. La muestra turquesa en (192,80) era (23,107,91,255). Estas muestras comprueban la abertura y el relleno de este archivo, no todos los píxeles del borde ni todas las funciones SVG.

Los rectángulos blancos de la página del navegador son fondos CSS detrás de los canvas. No son pintura blanca añadida al PNG. Si tu resultado necesita un fondo opaco, píntalo explícitamente antes de dibujar la imagen y vuelve a verificar el archivo exportado.

Separa el maestro editable del PNG

El PNG guardado contiene píxeles. No lleva el trazado, el círculo ni el contorno interior editable originales. Conserva master.svg como origen vectorial y elige las dimensiones del mapa de bits del canvas para el destino que realmente necesite PNG. Ampliar un PNG existente después no restaura su geometría de origen.

Un nombre de archivo SVG por sí solo tampoco demuestra que todo el diseño interior sea geometría vectorial; usa la guía de inspección de ráster incrustado cuando un archivo contenga imágenes enlazadas o incrustadas.

Si tu diseño de origen solo existe como imagen ráster y necesitas contornos editables, el flujo de imagen a vector de PerfectVector puede preparar un maestro SVG. Inspecciona su contorno, colores y aberturas antes de usarlo en esta rutina de carga. La vectorización no corrige encabezados CORS ni concede permiso de lectura del canvas.

Preguntas frecuentes

¿Puedo pasar marcado SVG directamente a drawImage()? No. Este ejemplo carga un archivo SVG en un HTMLImageElement y pasa la imagen decodificada a drawImage(). Una cadena de marcado no es ese objeto de imagen. Separa la carga del documento de la construcción de datos de trazado con Path2D.

¿Por qué aparece la imagen pero falla la exportación PNG? Una imagen de otro origen cargada sin permiso CORS puede contaminar el canvas al dibujarla. En nuestro caso sin CORS, el dibujo funcionó, pero tanto la lectura de píxeles como la exportación lanzaron SecurityError. Revisa el modo de solicitud y la respuesta del servidor en lugar de suponer que la geometría SVG está dañada.

¿crossOrigin anonymous corrige cualquier URL de imagen? No. Establécelo antes de src, y el servidor de imágenes debe proporcionar una respuesta CORS compatible. Nuestro mismo diseño falló al decodificar cuando se solicitó anonymous al endpoint sin el encabezado de permiso.

¿Exportar canvas como PNG conserva trazados SVG editables? No. PNG es una salida ráster. Conserva el SVG original por separado si necesitas editar trazados, formas o aberturas y revisa el PNG al tamaño de entrega previsto.

Fuentes

  1. MDN: CanvasRenderingContext2D.drawImage() — Objetos de imagen aceptados, tamaño de destino y dimensiones intrínsecas.
  2. MDN: HTMLImageElement.decode() — Promesa de disponibilidad de la imagen y rechazo por solicitud o decodificación.
  3. MDN: Imágenes con CORS — Restricciones de origen limpio para leer y exportar imágenes dibujadas.
  4. MDN: HTMLImageElement.crossOrigin — Modo de solicitud CORS para cargar imágenes.
  5. MDN: HTMLCanvasElement.toBlob() — Generación de blobs PNG, salida nula y SecurityError.
  6. MDN: SVG como imagen — Contextos de imagen SVG y restricciones de recursos externos.

¿Partes de un ráster y necesitas un diseño editable? Crea un maestro SVG con PerfectVector, inspecciona su contorno y sus aberturas, y conserva ese maestro mientras revisas por separado el renderizado del canvas y el PNG exportado.

Más del blog

PerfectVector

Empieza con un SVG más limpio
y fácil de editar

Sin tarjeta de crédito