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.
Los dos métodos de acceso a píxeles son los únicos de la API que operan sobre el búfer sin pasar por el modelo de dibujo, y esa excepcionalidad tiene consecuencias en cadena: ignoran la matriz de transformación, ignoran la composición, ignoran el recorte parcialmente, y están sujetos a una restricción de seguridad que puede inutilizar un canvas para siempre.
- Leer y escribir regiones de píxeles con las firmas completas de ambos métodos.
- Enumerar qué partes del estado del contexto ignoran y por qué.
- Explicar la contaminación del canvas por origen cruzado y cómo evitarla.
- Usar el rectángulo sucio de
putImageDatapara escribir solo una parte.
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 transformaciones
const 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 src
img.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; }
}
El búfer del canvas guarda los colores con alfa premultiplicado: cada canal ya viene multiplicado por el alfa. ImageData, en cambio, está definido con alfa no premultiplicado. Eso significa que getImageData divide por el alfa y putImageData vuelve a multiplicar, y ambas operaciones ocurren en ocho bits por canal. Con alfa 255 la ida y vuelta es exacta. Con alfa bajo es catastrófica: un píxel con alfa 10 tiene sus canales de color almacenados con solo diez niveles útiles de los 255 posibles, y al des-premultiplicarlos el resultado se redondea a múltiplos de unos 25. Volver a escribirlo pierde otro tanto. Las consecuencias son tres y las tres aparecen en producción. Primera: getImageData seguido de putImageData sin modificar nada no es la identidad sobre zonas semitransparentes. Si necesitas copiar píxeles sin pérdida, usa drawImage, que se queda en el espacio premultiplicado. Segunda: un filtro que se aplica repetidamente sobre el mismo canvas —un efecto de estela, un desenfoque acumulativo, un procesado en cada fotograma— degrada progresivamente las zonas suaves, y el síntoma es que los bordes difuminados se van ensuciando con el tiempo sin que nada en el código lo explique. Tercera: los valores de color de un píxel con alfa cero son basura. Un píxel totalmente transparente no tiene color recuperable, porque cualquier color multiplicado por cero da cero. Si tu filtro hace algo con esos píxeles, hará algo con ruido. La defensa cuando el procesado tiene que ser repetido es mantener el ImageData como fuente de verdad, procesarlo en memoria tantas veces como haga falta y escribirlo una sola vez al final, en lugar de ir y venir del canvas en cada iteración.
Construir un ImageData desde cero
No hace falta leer del canvas para tener un ImageData. Se puede construir:
// Vacio, transparente
const d1 = ctx.createImageData(256, 256);
// Con las mismas dimensiones que otro
const d2 = ctx.createImageData(otroImageData);
// Desde un array existente
const bytes = new Uint8ClampedArray(256 * 256 * 4);
const d3 = new ImageData(bytes, 256, 256);
// Solo dimensiones, con espacio de color
const 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 final
function 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 canvas
ctx.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.