getImageData y putImageData: el acceso directo
Leer y escribir píxeles del canvas, conocer las reglas que rompen la simetría con el resto de la API, y las restricciones de seguridad que las gobiernan.
Las firmas
const datos = ctx.getImageData(x, y, ancho, alto, ajustes);ctx.putImageData(datos, dx, dy);ctx.putImageData(datos, dx, dy, sucioX, sucioY, sucioAncho, sucioAlto);getImageData devuelve un objeto ImageData con tres propiedades: width, height y data, un Uint8ClampedArray con cuatro bytes por píxel. El parámetro opcional de ajustes admite colorSpace, para pedir los datos en un espacio concreto.
putImageData escribe ese array en el canvas. La versión larga escribe solo el subrectángulo indicado, lo cual permite actualizar una parte pequeña de un ImageData grande sin reconstruirlo.
Las coordenadas de ambos son píxeles del búfer, no unidades del espacio de usuario. Si el contexto está escalado por la densidad de pantalla, hay que multiplicar por ella.
const dpr = window.devicePixelRatio || 1;// Leer la region logica (10, 10, 100, 50)const d = ctx.getImageData(10 * dpr, 10 * dpr, 100 * dpr, 50 * dpr);Todo lo que ignoran
Esta lista es la que rompe expectativas y la que hay que memorizar.
Ignoran la matriz de transformación. Ni la escala, ni la traslación, ni la rotación. Trabajan sobre el búfer plano.
putImageData ignora globalAlpha. Escribir píxeles con alfa 128 no los hace más transparentes por tener globalAlpha a 0,5; se escriben tal cual.
putImageData ignora globalCompositeOperation. No compone: sustituye. Los píxeles escritos reemplazan por completo a los que había, incluido su canal alfa. Escribir un píxel totalmente transparente borra lo que hubiera debajo.
putImageData ignora las sombras y los filtros.
Sí respetan la región de recorte. Este es el único elemento del estado que se aplica, y es fácil de olvidar.
Esa combinación hace que putImageData no sea la forma de dibujar una imagen. Si lo que quieres es componer píxeles sobre lo que ya hay, la herramienta es drawImage desde un canvas auxiliar o desde un ImageBitmap creado a partir del ImageData:
// Componer un ImageData respetando alfa, composicion y transformacionesconst bitmap = await createImageBitmap(datos);ctx.globalAlpha = 0.5;ctx.drawImage(bitmap, x, y);bitmap.close();Fuera de los límites
Leer una región que se sale del canvas no lanza error: los píxeles fuera del búfer se devuelven como negro transparente, es decir, ceros en los cuatro canales. Es un comportamiento cómodo que permite leer con margen sin comprobar los bordes.
Escribir fuera de los límites tampoco lanza: se descarta lo que caiga fuera.
Lo que sí lanza es pedir un ancho o alto de cero: IndexSizeError.
La contaminación del canvas
Esta es la restricción de seguridad más importante del canvas y hay que entenderla bien porque no tiene rodeo.
Si dibujas en un canvas una imagen, un vídeo o cualquier recurso de otro origen sin permiso explícito de CORS, el canvas queda marcado como contaminado. A partir de ese momento, cualquier intento de leer sus píxeles lanza un SecurityError:
getImageDatatoDataURLtoBlobcreateImageBitmapsobre ese canvastransferToImageBitmapen unOffscreenCanvas
La razón es evidente en cuanto se ve el ataque que impide: sin ella, cualquier página podría dibujar una imagen privada de otro dominio —una foto de perfil, un documento— y leer sus píxeles, exfiltrando contenido al que no tiene acceso.
La única forma de evitarlo es que el recurso llegue con permiso explícito, lo que exige dos cosas a la vez:
const img = new Image();img.crossOrigin = 'anonymous'; // 1. pedirlo desde el cliente, ANTES de srcimg.src = 'https://cdn.example/foto.jpg';Y 2. que el servidor responda con la cabecera de control de acceso adecuada. Si falta cualquiera de las dos, la imagen se dibuja igual pero el canvas queda contaminado.
Dos detalles que ahorran horas de depuración. El primero: la contaminación es irreversible. No hay forma de limpiar un canvas contaminado; hay que crear uno nuevo. El segundo: una imagen cacheada sin CORS sigue contaminando. Si la misma URL se cargó antes sin crossOrigin, el navegador puede servir la respuesta cacheada, que no tiene las cabeceras, y contaminar de todas formas. La solución es usar URLs distintas o forzar la revalidación.
function estaContaminado(canvas) { try { canvas.getContext('2d').getImageData(0, 0, 1, 1); return false; } catch { return true; }}Construir un ImageData desde cero
No hace falta leer del canvas para tener un ImageData. Se puede construir:
// Vacio, transparenteconst d1 = ctx.createImageData(256, 256);// Con las mismas dimensiones que otroconst d2 = ctx.createImageData(otroImageData);// Desde un array existenteconst bytes = new Uint8ClampedArray(256 * 256 * 4);const d3 = new ImageData(bytes, 256, 256);// Solo dimensiones, con espacio de colorconst d4 = new ImageData(256, 256, { colorSpace: 'display-p3' });El constructor directo es el que más interesa cuando generas la imagen por completo, porque evita crear un canvas solo para pedir el objeto.
// Generar un degradado procedural sin tocar el canvas hasta el finalfunction degradadoProcedural(ancho, alto) { const bytes = new Uint8ClampedArray(ancho * alto * 4); for (let y = 0; y < alto; y++) { for (let x = 0; x < ancho; x++) { const i = (y * ancho + x) * 4; bytes[i] = (x / ancho) * 255; bytes[i + 1] = (y / alto) * 255; bytes[i + 2] = 180; bytes[i + 3] = 255; } } return new ImageData(bytes, ancho, alto);}ctx.putImageData(degradadoProcedural(300, 200), 0, 0);El rectángulo sucio
La versión larga de putImageData escribe solo una parte del ImageData:
ctx.putImageData(datos, dx, dy, sucioX, sucioY, sucioAncho, sucioAlto);Los cuatro últimos parámetros son coordenadas dentro del ImageData, no del canvas. El píxel (sucioX, sucioY) del origen acaba en (dx + sucioX, dy + sucioY) del canvas.
Sirve para el patrón de mantener un búfer grande en memoria y volcar solo la región que ha cambiado, que es exactamente lo que hace un simulador o un editor de píxeles:
// Solo la zona modificada se escribe al canvasctx.putImageData(mundo, 0, 0, minX, minY, maxX - minX + 1, maxY - minY + 1);El ahorro es proporcional a la fracción del área que se escribe, y en simulaciones donde solo cambia una parte pequeña es la diferencia entre fluido y no fluido.