En ocasiones es necesario leer los pixeles y colores de una imagen con JavaScript del lado del cliente para conocer los valores RGBA o red, green, blue y alpha (rojo, verde, azul, nivel alfa) sin necesidad de algún servidor.
A lo largo de este artículo te voy a enseñar cómo recorrer los pixeles de una imagen con JavaScript del lado del cliente usando OffscreenCanvas
y getImageData
para leer los pixeles de una imagen desde el client-side.
Prueba la demostración ya mismo en el siguiente enlace. Asegúrate de abrir la consola de depuración para que puedas apreciar cómo se imprime el RGBA de cada pixel de la imagen seleccionada: https://parzibyte.github.io/ejemplos-javascript/pixeles-imagen/
El ejemplo que te mostraré va a leer cada pixel de la imagen a partir de un input
de tipo file
, pero como siempre te digo: la imagen puede venir de cualquier lugar.
Leer los pixeles de la imagen con JS sirve para esconder texto en una imagen con Esteganografía, por poner un ejemplo.
Explicación del algoritmo
En términos simples, vamos a leer cada pixel de una imagen con JS desde el navegador web; sin pintar la imagen en un canvas visible para el usuario, por ello usaremos OffscreenCanvas
y funciones asíncronas.
Vamos a:
- Colocar un
input
de tipofile
- Escuchar cuando el usuario seleccione un archivo
- Convertir ese archivo a bitmap
- Dibujar el bitmap en un canvas fuera de pantalla
- Obtener los pixeles de la imagen como un arreglo de una dimensión e iterarlo obteniendo los valores R, G, B y A
- Imprimir la información de cada pixel, es decir, los colores y también el nivel alfa
Escuchar cambio de archivo seleccionado
Comencemos viendo el input de tipo file con el cual se va a seleccionar la imagen:
<input type="file" id="imagen">
Obtenemos una referencia al mismo con querySelector
y escuchamos el evento change
:
const $imagen = document.querySelector("#imagen");
$imagen.addEventListener("change", async () => {
// Leer imagen aquí
});
Los archivos están en la propiedad files
del input, que es un arreglo que contiene los archivos seleccionados. Validamos que exista al menos un archivo y accedemos al primero de ellos:
const imagenesSeleccionadas = $imagen.files;
if (imagenesSeleccionadas.length <= 0) {
return;
}
const primerArchivoDeImagen = imagenesSeleccionadas[0];
Fíjate bien en la constante primerArchivoDeImagen
. Este es un archivo de tipo File
(valga la redundancia), no es una imagen de tipo Image
. Vamos a trabajar únicamente con esa constante, convertirla a bitimage y dibujarla en el canvas.
Nota: por simplicidad, el código presentado no hace la validación de que el archivo sea realmente una imagen.
Dibujando imagen en OffscreenCanvas
Sí, es necesario dibujar la imagen en un canvas (fuera de pantalla) para poder acceder a sus pixeles. No te preocupes, esto está totalmente optimizado y no bloquea la interfaz porque es una función asíncrona.
const imagenComoBitmap = await createImageBitmap(primerArchivoDeImagen);
const canvasFueraDePantalla = new OffscreenCanvas(imagenComoBitmap.width, imagenComoBitmap.height);
const contexto = canvasFueraDePantalla.getContext("2d");
contexto.drawImage(imagenComoBitmap, 0, 0, imagenComoBitmap.width, imagenComoBitmap.height);
La imagen ya está dibujada en el lienzo “invisible”, y al ser un canvas podemos hacer casi todas las operaciones que haríamos en un canvas normal.
Obtener pixeles
Ahora que tenemos acceso al contexto del canvas podemos invocar a getImageData
que devolverá, entre otros datos, los pixeles en la propiedad data
.
Al invocar a getImageData
se nos devuelve un arreglo de una dimensión en donde vienen los valores en el orden de R, G, B, A, R, G, B, A… dicho con otras palabras, cada pixel ocupa 4 posiciones y la longitud del array es un número múltiplo de 4.
El código para leer los valores RGBA de una imagen con JS solo usando el navegador queda así:
const posicionesPorPixel = 4;
const datosDeImagen = contexto.getImageData(0, 0, imagenComoBitmap.width, imagenComoBitmap.height);
const pixeles = datosDeImagen.data;
for (let indice = 0; indice < pixeles.length; indice += posicionesPorPixel) {
const rojo = pixeles[indice];
const verde = pixeles[indice + 1];
const azul = pixeles[indice + 2];
const alpha = pixeles[indice + 3];
console.log(`rgba(${rojo}, ${verde}, ${azul}, ${alpha})`);
}
En cada paso del ciclo estamos leyendo exitosamente el RGBA de una imagen e imprimiéndolo en la consola de depuración. Por cierto, el orden de recorrido de los pixeles comienza en x=0
,y=0
y termina en x=ancho de imagen - 1
, y=alto de imagen - 1
.
Dicho con otras palabras, el recorrido es desde la esquina superior izquierda hasta la esquina inferior derecha, o de izquierda a derecha y de arriba hacia abajo.
Poniendo todo junto
La demostración está en el siguiente enlace. No te olvides de abrir la consola de depuración. Recomiendo abrir una imagen no tan grande: https://parzibyte.github.io/ejemplos-javascript/pixeles-imagen/
El código completo (HTML y JavaScript) lo encuentras en GitHub: https://github.com/parzibyte/ejemplos-javascript/tree/master/pixeles-imagen
Nota importante: en la captura, la imagen es muy pequeña (de 3×4 pixeles) y se ve como pixel art. Lo he hecho así con fines demostrativos (usando GIMP para hacer ese zoom), pero funciona con cualquier imagen y cualquier formato. He probado con jpg, png y webp.
Por cierto, si te lo preguntas, he usado este código en mi diseñador de recibos ESC POS para impresoras térmicas. El código, no tan ordenado, está en: https://github.com/parzibyte/esc-pos-ticket-designer/blob/main/src/components/Operaciones/DefinirCaracterPersonalizado.vue#L201