Manipulación de imágenes con Javascript. Parte 1

03 Nov 2016

Introducción

En este artículo vamos a introducir la técnica para manipular imágenes utilizando el lenguaje Javascript. El objetivo es llegar a conocer y saber aplicar algunos de los procedimientos utilizados por los principales paquetes de software de edición de imagen en un entorno web.

Entre otras cosas, estudiaremos cómo cargar imágenes en memoria, modificar el brillo y contraste, aplicar filtros personalizados, invertir el color o guardar el resultado obtenido.

Porqué usar Javascript

Si bien es cierto que a día de hoy se pueden conseguir multitud de efectos con los filtros CSS, la idea de este artículo es aprender a manipular imágenes a través de su información original a bajo nivel. Esto presenta una serie de ventajas frente a CSS:

  • Conseguimos un control absoluto sobre los parámetros de la imagen, permitiéndonos así alterarla con absoluta libertad.
  • Podemos implementar nuestros propios filtros, ya sean de inveción propia, u otros ya sobradamente conocidos.
  • Al manipular directamente los datos de una imagen, resulta muy sencillo guardar los resultados obtenidos.
  • De forma complementaria, permite aprender la algoritmia básica que utilizan desde hace años los paquetes de edición más conocidos del mercado.

¡Y usamos Javascript porque nos gusta escribir código a mansalva y los filtros CSS le quitarían a eso toda la gracia!

Cómo cargar imágenes

Para manipular una imagen en Javascript, el primer paso es cargarla en memoria. Para ello, tenemos varias posibilidades:

  • Utilizar la API FileReader permitiendo al usuario subir una imagen desde su equipo o dispositivo.
  • Utilizar la API Web Workers para pre cargar la imagen en memoria desde una URL.
  • Utilizar el elemento canvas para dibujar o incrustar gráficos.
  • Y tantas más…

Para este artículo, vamos a utilizar el apoyo de la etiqueta ‘canvas’, un elemento con soporte completo en la totalidad de navegadores modernos y, al mismo tiempo, habitual en las herramientas web de edición de imágenes.

<canvas> es un nuevo elemento HTML que puede usarse para dibujar gráficos a través de scripting (normalmente Javascript). Por ejemplo, puede emplearse para hacer composición de fotos, crear animaciones e incluso procesamiento de vídeo en tiempo real.

MDN, Canvas

El código HTML base que usaremos en este ejercicio es el siguiente:

<!DOCTYPE html>
<html>
    <head>
        <title>Testing canvas</title>
        <style>
            body {
                background: #333;
            }
            #main {
                margin-top: 100px;
                text-align: center;
            }
            #canvas {
                background-color: #000;
                height: 600px;
                width: 800px;
            }
        </style>
    </head>
    <body>
        <div id="main">
            <canvas id="canvas" width="800" height="600"></canvas>
        </div>
 
        <script></script>
    </body>
</html>

Tenemos ahí una etiqueta canvas con unas medidas concretas (800×600 píxeles); también algo de estilo CSS (completamente opcional) para que el resultado sea más atractivo (centrando el canvas y añadiendo un color de fondo oscuro de fondo). Finalmente, una etiqueta ‘script‘ vacía que cierra el ‘body‘ donde añadamos el código Javascript para trabajar con la imagen.

El resultado en el navegador debería ser similar a esto:

canvas-empty

Nuestra aplicación, de momento vacía

Necesitamos también una imagen para nuestras pruebas:

entropy

entropy.jpg

Hemos seleccionado esa fotografía libre de derechos (fuente original) por la cantidad de información de color que contiene. Por otro lado, aunque el original es de un tamaño superior, hemos optado por reducirlo hasta el tamaño exacto de nuestro contenedor (800×600).

Preparando el Javascript base para nuestro ejercicio

Es turno ahora del código Javascript. No usaremos ninguna dependencia, y recurriremos a un patrón de diseño de tipo módulo.

var app = ( function () {
    var canvas = document.getElementById( 'canvas' ),
        context = canvas.getContext( '2d' ),
 
        // API
        public = {};
 
        // Public methods goes here...
 
        return public;
} () );

Este pequeño fragmento lo colocamos entre las etiqueta script que dejamos antes vacías en el HTML. Por el momento, nos limitamos a seleccionar el elemento ‘canvas‘ desde el DOM a través de su ID y a continuación, su contexto 2D.

Siguiendo las buenas prácticas, hemos creado un objeto ‘public‘ que será el contenga los métodos -públicos- (nuestra pequeña API) para trabajar. Eso nos permite emular una ‘clase’ con métodos públicos y privados para nuestra aplicación.

NOTA: Para este ejemplo, hemos decidido mantener una sintaxis Javascript clásica, evitando con ello el uso de las nuevas clases de ES6, las funciones flecha, etc… En la segunda parte, introduciremos algunas mejoras y actualizaremos el código a los estándares modernos.

Para cargar la imagen dentro del objeto canvas, implementamos nuestro primer método. Éste se encargará de crear una etiqueta <img>, asociándole la fotografía que hemos seleccionado como src para, finalmente, dibujarla dentro del contexto 2D:

public.loadPicture = function () {
    var imageObj = new Image();
    imageObj.src = 'entropy.jpg';
 
    imageObj.onload = function () {
        context.drawImage( imageObj, 0, 0 );
    }
};

NOTA: La ruta de la imagen debe ajustarse según vuestro entorno de pruebas.

El bloque anterior se añadiría dentro de nuestro objeto ‘app‘, tras la definición del objeto ‘public‘ (el código completo se encuentra al final del artículo).

Podemos probar nuestro método desde la consola usando como prefijo el nombre que hemos dado a nuestra aplicación:

app.loadPicture();

El resultado debería mostrar nuestra imagen cargada dentro del objeto canvas:

canvas-loaded

Nuestra imagen ya cargada en el elemento canvas

Cómo se descompone una imagen en datos

Aquí es donde ponemos el foco sobre el tema principal del artículo.

Para manipular una imagen, necesitamos trabajar con ella a nivel de datos, y con esto queremos decir, a nivel de píxeles. Para ello, canvas proporciona un método muy práctico que, aplicado sobre su contexto, extrae toda la información sobre su contenido: getImageData. Lo implementamos en nuestro ejemplo con un método:

public.getImgData = function () {
    return context.getImageData( 0, 0, canvas.width, canvas.height );
};

NOTA: Los parámetros que requiere getImageData son los pares de coordenadas X e Y (inicio y final) que queremos leer. La documentación al respecto la podemos encontrar aquí.

NOTA 2: Aunque hemos definido nuestro propio método getImgData como público (ya que está extendiendo al objeto ‘public’), en un contexto real, debería ser privado. Si hemos optado por esta opción, es para que el lector pueda jugar desde la consola de su navegador con todos estas funciones.

Para echarle un vistazo a los datos que configuran la imagen, lanzamos desde la consola nuestro nuevo método (de ahí que sea práctico haberlo hecho público):

app.getImgData();
 
// ImageData { width=800,  height=600,  data=Uint8ClampedArray }

Aquí, lo que realmente nos interesa, es el valor de ‘data‘: un array tipado (de tipo Uint8ClampedArray) que es el que tendremos que manipular para poder jugar con nuestra imagen.

Esa matriz es la clave, y por ello, comprender sus valores es importante (aunque de entrada resulten algo confusos):

Uint8ClampedArray { 0=29,  1=91,  2=168,  más...}

Los datos de una imagen forman una matriz de tipo rejilla donde a cada píxel le corresponden 4 valores. Cada uno de estos son los que a su vez se identifican con los valores R (rojo), G (verde), B (azul) y A (transparencia) de dicho píxel.

pixel-grid

El rango de valores de cada uno de estos parámetros va entre 0 y 255, indicando con el mismo la intensidad/saturación de color. De este modo, la suma de los cuatro factores determina el color exacto de cada píxel según la intersección correspondiente en el modelo RGB:

rgb-wheel

RGB es un modelo de color basado en la síntesis aditiva, con el que es posible representar un color mediante la mezcla por adición de los tres colores de luz primarios.

Wikipedia, RGB

De lo anterior se deduce que, en nuestro caso, como la imagen es de 800×600 píxeles y a cada uno le corresponden 4 valores en la matriz, la longitud total de nuestro array es de 800 x 600 x 4 = 1.920.000 elementos. Podemos comprobar este dato fácilmente desde la consola:

app.getImgData().data.length; // 1920000

Como vemos, el espacio en memoria que necesitamos para trabajar con una imagen se corresponde de forma directa con sus dimensiones (tamaño en píxeles). Esto es un aspecto importante para medir el rendimiento de nuestra aplicación ya que, imágenes mayores, requieren mayor memoria y mayor tiempo de procesado en lote. Podemos hacernos una idea de la rápida progresión con la siguiente tabla:

Ancho Alto Píxeles
640 360 921.600
800 600 1.920.000
1024 768 3.145.728
1280 720 3.686.400
1920 1080 8.294.400

NOTA: Los tamaños de ahí arriba son algunas de las resoluciones consideradas estándar en distintos sistemas y pantallas. Fuente tomada de aquí

¿Y si no usamos canvas?

Como ya se ha indicado, el elemento canvas nos proporciona una interfaz directa y nativa para trabajar con píxeles a través de ImageData.

En el ejemplo anterior hemos accedido a esta capa a través del método getImageData que aplicamos sobre el CanvasRenderingContext2D (contexto del elemento canvas).

Si no disponemos de este elemento, leer la informaciòn de una imagen se vuelve significativamente más complejo: lo habitual es recrear este escenario para así poder contar con el método nativo. Esto quiere decir que, aunque no dispongamos de un elemento canvas previo en el DOM, podemos crear uno bajo demanda en memoria para así acceder a los datos de la imagen:

var canvas = document.createElement( 'canvas' ),
    context = canvas.getContext( '2d' ),
    img = document.getElementById( 'my-img' );
 
canvas.width = img.width;
canvas.height = img.height;
context.drawImage( img, 0, 0 );
 
var imageData = context.getImageData( 0, 0, img.width, img.height );

Donde la variable ‘img‘ recogería nuestra imagen desde la fuente que provenga (en este caso, un elemento <img> en el DOM, pero perfectamente podría seguirse el esquema del ejemplo anterior).

Si no es posible utilizar ‘canvas‘, nos veremos obligados al uso de bibliotecas de terceros que realicen una decodificación píxel a píxel de la imagen según su formato.

Un ejemplo de este tipo de herramientas puede ser ‘get-pixels’ (enlace). Este script toma una imagen a partir de una URL y devuelve un array de píxeles usando únicamente Javascript. La URL puede ser una ruta relativa, una ruta HTTP, datos codificados o un ArrayBuffer.

Estas herramientas dependen a su vez de bibliotecas externas para decodificar cada posible formato (a saber, jpeg, png o gif) y a veces se pueden producir imprecisiones en la obtención del color exacto de un determinado píxel. Por lo general, el error suele ser del orden de +-1 (para un rango de 0 a 255), sin embargo, dependiendo de la finalidad de nuestra herramienta (por ejemplo en análisis de color), ese error puede ser inadmisible.

Iteración básica de píxeles

Volviendo a nuestra matriz de datos, ya sea la generada con canvas, o con una biblioteca externa, tenemos que ser capaces de iterar por cada píxel y sus cuatro valores RGBA asociados.

Para ello, utilizaremos un bucle ‘for‘ estándar:

var imageData = app.getImageData(),
    pixels = imageData.data,
    numPixels = imageData.width * imageData.height;
 
for ( var i = 0; i < numPixels; i++ ) {
    var r = pixels[ i * 4 ],
        g = pixels[ i * 4 + 1 ],
        b = pixels[ i * 4 + 2 ];
}

Como conocemos el número total de píxeles que componen nuestra imagen (resultado de multiplicar la anchura de nuestra imagen por su altura), podemos seleccionarlos uno a uno desde nuestra matriz y almacenar (cachear) sus valores RGBA en nuestras propias variables r, g y b. En este caso, vemos que hemos despreciado el canal alpha ya que, en la práctica, rara vez tendremos que hacer uso de él.

Una vez hemos conseguido ‘tocar’ cada valor de un píxel de forma secuencial, podemos cambiarlo y devolverlo después a la matriz (ya modificado) para repintar la imagen.

Para poner todo en práctica, vamos a jugar con lo que habitualmente llamamos en edición de imágenes ‘los filtros’.

Filtros

Es frecuente a día de hoy ver aplicaciones web y móviles que modifican nuestras fotografías aplicándoles filtros. Los paquetes tradicionales de software de escritorio llevan décadas trabajando los mismos resultados con técnicas similares a las que vamos a ver a continuación.

La idea básica es aquí conocer el algoritmo que debe aplicarse sobre cada píxel para conseguir el efecto deseado. Y, afortunadamente, estos algoritmos suelen ser de dominio público en su mayoría: una búsqueda rápida en Google nos pondrá rápidamente sobre la pista de aquel que necesitamos.

Filtro para eliminar el color de una imagen (pasar a blanco y negro)

Quizá este filtro es el más antiguo de todos. La idea es sumamente simple: eliminar la información de color de una imagen para pasarla a escala de grises.

El algoritmo es trivial, y se corresponde con la siguiente fórmula:

Gray = ( Red + Green + Blue ) / 3

En pseudo código:

For Each Pixel in Image {
 
   Red = Pixel.Red
   Green = Pixel.Green
   Blue = Pixel.Blue
 
   Gray = ( Red + Green + Blue ) / 3
 
   Pixel.Red = Gray
   Pixel.Green = Gray
   Pixel.Blue = Gray
 
}

NOTA: La anterior, es una de las muchas fórmulas que podemos encontrar (aquí lás más sencillas y conocidas). Existen otras formulaciones mucho más complejas para conseguir efectos más precisos según el tipo de imagen original sobre el que se apliquen. Para más información al respecto, podemos ver el correspondiente artículo en Wikipedia.

Conociendo la fórmula, aplicarla en nuestro código es muy sencillo:

// Filters
public.filters = {};
 
public.filters.bw = function () {
    var imageData = app.getImgData(),
        pixels = imageData.data,
        numPixels = imageData.width * imageData.height;
 
    for ( var i = 0; i < numPixels; i++ ) {
        var r = pixels[ i * 4 ];
        var g = pixels[ i * 4 + 1 ];
        var b = pixels[ i * 4 + 2 ];
 
        var grey = ( r + g + b ) / 3;
 
        pixels[ i * 4 ] = grey;
        pixels[ i * 4 + 1 ] = grey;
        pixels[ i * 4 + 2 ] = grey;
    }
 
    context.putImageData( imageData, 0, 0 );
};

Hemos creado un nuevo objeto, ‘filters‘, donde iremos agrupando nuestros filtros a medida los vayamos implementando. También hemos creado el primero, ‘bw’ (abreviatura de ‘black and white’), donde hemos reproducido el algoritmo comentado anteriormente.

La última instrucción ‘putImageData‘ es importante ya que es la que se encarga de redibujar la imagen volcando los nuevos píxeles ya modificados.

Para probar el filtro desde la consola, nos basta con llamar al método desde nuestra API:

app.filters.bw();

El resultado debería similar al que se ve a continuación:

filter-bw

¡Nuestro primer filtro!

Un apunte interesante es que entender que, este tipo de filtros donde se destruye información (en este caso cromática), no es reversible. Dicho de otro modo: no es posible reconstruir el color de una imagen en blanco y negro a partir de un algoritmo natural. Actualmente, los sistemas más precisos para colorear fotografías en blanco y negro lo hacen en a través a análisis predictivos basados en redes neuronales. Puede leerse todo el proceso y la algoritmia necesaria para su implementación en un artículo editado por la Waseda University [PDF].

Aviso general sobre el código

Para mantener la máxima claridad posible, todos los ejemplos que vamos a implementar en este primer artículo van a trabajar de forma independiente los unos de los otros. Esto quiere decir que veremos duplicidad y que, por tanto, podría aplicarse un refactorizado importante a todo el conjunto final.

La ventaja didáctica de esta duplicidad Es que nos permite comprobar cómo es posible implementar prácticamente cualquier algoritmo que encontremos en Internet en nuestra aplicación sin apenas alterarlo.

Filtro para invertir los colores de una imagen

Otro filtro que desde siempre nos ha acompañado en los programas de edición gráfica es el que nos permite invertir la información cromática de una imagen. Técnicamente, convierte una imagen positiva en negativo y viceversa.

En este caso, el algoritmo (también muy sencillo) sería el siguiente:

Red = 255 - r
Green = 255 - g
Blue = 255 - b

O, en pseudo código:

For Each Pixel in Image {
 
   Red = Pixel.Red
   Green = Pixel.Green
   Blue = Pixel.Blue
 
   Pixel.Red = 255 - Red
   Pixel.Green = 255 - Green
   Pixel.Blue = 255 - Blue
 
}

Para implementarlo, repetimos el esquema anterior (repetimos que esta duplicidad es únicamente para ejemplificar este artículo):

public.filters.invert = function () {
    var imageData = app.getImgData(),
        pixels = imageData.data,
        numPixels = imageData.width * imageData.height;
 
    for ( var i = 0; i < numPixels; i++ ) {
        var r = pixels[ i * 4 ];
        var g = pixels[ i * 4 + 1 ];
        var b = pixels[ i * 4 + 2 ];
 
        pixels[ i * 4 ] = 255 - r;
        pixels[ i * 4 + 1 ] = 255 - g;
        pixels[ i * 4 + 2 ] = 255 - b;
    }
 
    context.putImageData( imageData, 0, 0 );
};

Para aplicarlo, recurrimos de nuevo a nuestra API desde la consola:

app.filters.invert();

Y el resultado sería:

filter-invert

Inversión de colores. Útil para obtener un positivo desde una imagen en negativo.

Filtro sepia

Pocos filtros han ganado más aceptación a lo largo de los últimos 5 años que aquellos de dan un aspecto ‘vintage‘ a nuestra imágenes. En ese campo, el filtro sepia es siempre una aproximación agradable.

De nuevo, tenemos muchos algoritmos que producen efectos similares (el cual, dicho sea de paso, se continúa revisando activamente). Para nuestro artículo hemos optado por este:

Red = (r * .393) + (g * .769) + (b * .189)
Green = (r * .349) + (g * .686) + (b * .168)
Blue = (r * .272) + (g * .534) + (b * .131)

La implementación (obviamos el pseudo código a estas alturas):

public.filters.sepia = function () {
    var imageData = app.getImgData(),
        pixels = imageData.data,
        numPixels = imageData.width * imageData.height;
 
    for ( var i = 0; i < numPixels; i++ ) {
        var r = pixels[ i * 4 ];
        var g = pixels[ i * 4 + 1 ];
        var b = pixels[ i * 4 + 2 ];
 
        pixels[ i * 4 ] = 255 - r;
        pixels[ i * 4 + 1 ] = 255 - g;
        pixels[ i * 4 + 2 ] = 255 - b;
 
        pixels[ i * 4 ] = ( r * .393 ) + ( g *.769 ) + ( b * .189 );
        pixels[ i * 4 + 1 ] = ( r * .349 ) + ( g *.686 ) + ( b * .168 );
        pixels[ i * 4 + 2 ] = ( r * .272 ) + ( g *.534 ) + ( b * .131 );
    }
 
    context.putImageData( imageData, 0, 0 );
};

Lo probamos:

app.filters.sepia();

Y obtenemos nuestra imagen modificada:

filter-sepia

Nuestra playa desde un punto de vista vintage.

Filtro para manipular contraste

Algunos filtros requieren además del algoritmo base, un valor o factor de referencia. Un ejemplo de esto es la manipulación del brillo o contraste en una imagen donde, por lo general, el usuario ajusta la cantidad deseada mediante algún control implementado en la UI.

Para este artículo, vamos a crear un filtro de contraste donde indicaremos el factor deseado directamente en la API.

El algoritmo se compone de dos pasos:

  • Cálculo del factor a partir de un valor dado.
  • Cálculo del valor de cada píxel tras aplicar el factor anterior.

Expresado como fórmula, tendríamos:

FACTOR = ( 259( C + 255 ) ) / ( 255( 259 - C ) )
 
Red = FACTOR * (r - 128) + 128;
Green = FACTOR * (g - 128) + 128;
Blue = FACTOR * (b - 128) + 128;

Implementado como método en nuestra aplicación quedaría así:

public.filters.contrast = function ( contrast ) {
    var imageData = app.getImgData(),
        pixels = imageData.data,
        numPixels = imageData.width * imageData.height,
        factor;
 
    contrast || ( contrast = 100 ); // Default value
 
    factor = ( 259 * ( contrast + 255 ) ) / ( 255 * ( 259 - contrast ) );
 
    for ( var i = 0; i < numPixels; i++ ) {
        var r = pixels[ i * 4 ];
        var g = pixels[ i * 4 + 1 ];
        var b = pixels[ i * 4 + 2 ];
 
        pixels[ i * 4 ] = factor * ( r - 128 ) + 128;
        pixels[ i * 4 + 1 ] = factor * ( g - 128 ) + 128;
        pixels[ i * 4 + 2 ] = factor * ( b - 128 ) + 128;
    }
 
    context.putImageData( imageData, 0, 0 );
};

Hemos puesto un valor por defecto para nuestro contraste de 100 (de entre un rango posible que va del -255 al 255).

Lo aplicamos un par de veces con dos valores diferentes:

app.contrast( 100 );
filter-contrast-100

El exceso de contraste produce una imagen quemada…
app.contrast( -200 );
filter-contrast-minus-200

… mientras que una carencia nos atenúa el motivo hasta casi el esbozo.

Más allá de los filtros básicos

Después de comprobar cómo se genera y manipula la información de una imagen, podemos pensar que únicamente podemos jugar con sus valores de color RGB a través de filtros (pasar a blanco y negro, invertir colores…). Sin embargo, cualquier paquete de software de edición nos demuestra que se pueden aplicar muchos más tipos de efectos diferentes.

El secreto para conseguir otros resultados es únicamente saber jugar con cada píxel y sus adyacentes; y ese juego nos lo dan siempre los algoritmos.

A medida que buscamos un post procesamiento más complejo, las fórmulas que lo permiten se vuelven también más largas y densas. En muchas ocasiones, un único algoritmo se basta para rellenar por sí solo un artículo científico con multitud de fórmulas.

Desenfoque

Veamos un ejemplo de sobra conocido por todos los aficionados al retoque fotográfico: el desenfoque gaussiano.

El desenfoque gaussiano mezcla ligeramente los colores de los píxeles que estén vecinos el uno al otro en un mapa de bits (imagen), lo que provoca que la imagen pierda algunos detalles minúsculos y, de esta forma, hace que la imagen se vea más suave (aunque menos nítida o clara) respecto a que los bordes presentes en la imagen se ven afectados.


Wikipedia, Desenfoque gaussiano

Este filtro, tan habitual en todo software de edición, esconde una cierta complejidad de cálculo. Es un ejemplo interesante de algoritmo que, pese a su frecuencia, no resulta sencillo de implementar.

De forma muy resumida, este efecto se calcula tomando como valor de cada píxel el valor medio de aquellos que lo rodean. He tomado esta imagen de un artículo en Gamasutra que sirve como ilustración práctica de este proceso:

gaussian-blur-sample

Las fórmulas originales necesarias para realizar el cálculo son algo más complejas que las vista hasta ahora (pueden verse aquí) por lo que esta aproximación directa no es recomendable en un lenguaje como Javascript.

Existen otras técnicas más livianas -pero con resultados peores- como es por ejemplo ‘Box blur’ (enlace).

Para conseguir un compromiso entre ambas técnicas (calidad frente a rendimiento) se suele utilizar una aproximación mixta que sí proporciona resultados óptimos. Un ejemplo de ello resulta muy extenso para colocarlo aquí, pero se puede consultar en este enlace y ver una demo aquí. Los enlaces anteriores se han revisado a fecha de este artículo (noviembre, 2016).

Guardando la imagen resultante

Una vez hemos aplicado uno o varios efectos sobre nuestra imagen, lo habitual es guardar el resultado.

En un escenario donde utilizamos la etiqueta canvas, esta acción no resulta muy intuitiva (al menos en un entorno multinavegador) y se suele utilizar un pequeño truco que convierte nuestra imagen en un enlace (anchor) sobre el que hacemos click de forma programática para forzar una descarga.

El método lo implementaríamos de la siguiente forma:

public.save = function () {
    var link = window.document.createElement( 'a' ),
        url = canvas.toDataURL(),
        filename = 'screenshot.jpg';
 
    link.setAttribute( 'href', url );
    link.setAttribute( 'download', filename );
    link.style.visibility = 'hidden';
    window.document.body.appendChild( link );
    link.click();
    window.document.body.removeChild( link );
};

El fragmento anterior se puede seguir fácilmente, y básicamente hace lo que hemos descrito más arriba: crear un enlace, hacer click sobre él y forzar una descarga con el navegador:

app.save();
image-saving

Con este simple truco, forzamos un diálogo de descarga

En nuestro ejemplo podemos ver cómo tras ejecutar el método anterior, el navegador nos muestra inmediatamente una ventana de descarga para así guardar nuestro trabajo.

Código completo

A modo recopilatorio, mostramos a continuación el código completo de nuestra mini aplicación, HTML incluido:

<!DOCTYPE html>
<html>
    <head>
        <title>Testing canvas</title>
        <style>
            body {
                background: #333;
            }
            #main {
                margin-top: 100px;
                text-align: center;
            }
            #canvas {
                background-color: #000;
                height: 600px;
                width: 800px;
            }
        </style>
    </head>
    <body>
        <div id="main">
            <canvas id="canvas" width="800" height="600"></canvas>
        </div>
 
        <script>
            var app = ( function () {
 
                var canvas = document.getElementById( 'canvas' ),
                    context = canvas.getContext( '2d' ),
 
                    // API
                    public = {};
 
                // Public methods goes here...
                public.loadPicture = function () {
                    var imageObj = new Image();
                    imageObj.src = 'entropy.jpg';
 
                    imageObj.onload = function () {
                        context.drawImage( imageObj, 0, 0 );
                    }
                };
 
                public.getImgData = function () {
                    return context.getImageData( 0, 0, canvas.width, canvas.height );
                };
 
 
                // Filters
                public.filters = {};
 
                public.filters.bw = function () {
                    var imageData = app.getImgData(),
                        pixels = imageData.data,
                        numPixels = imageData.width * imageData.height;
 
                    for ( var i = 0; i < numPixels; i++ ) {
                        var r = pixels[ i * 4 ];
                        var g = pixels[ i * 4 + 1 ];
                        var b = pixels[ i * 4 + 2 ];
 
                        var grey = ( r + g + b ) / 3;
 
                        pixels[ i * 4 ] = grey;
                        pixels[ i * 4 + 1 ] = grey;
                        pixels[ i * 4 + 2 ] = grey;
                    }
 
                    context.putImageData( imageData, 0, 0 );
                };
 
 
                public.filters.invert = function () {
                    var imageData = app.getImgData(),
                        pixels = imageData.data,
                        numPixels = imageData.width * imageData.height;
 
                    for ( var i = 0; i < numPixels; i++ ) {
                        var r = pixels[ i * 4 ];
                        var g = pixels[ i * 4 + 1 ];
                        var b = pixels[ i * 4 + 2 ];
 
                        pixels[ i * 4 ] = 255 - r;
                        pixels[ i * 4 + 1 ] = 255 - g;
                        pixels[ i * 4 + 2 ] = 255 - b;
                    }
 
                    context.putImageData( imageData, 0, 0 );
                };
 
                public.filters.sepia = function () {
                    var imageData = app.getImgData(),
                        pixels = imageData.data,
                        numPixels = imageData.width * imageData.height;
 
                    for ( var i = 0; i < numPixels; i++ ) {
                        var r = pixels[ i * 4 ];
                        var g = pixels[ i * 4 + 1 ];
                        var b = pixels[ i * 4 + 2 ];
 
                        pixels[ i * 4 ] = 255 - r;
                        pixels[ i * 4 + 1 ] = 255 - g;
                        pixels[ i * 4 + 2 ] = 255 - b;
 
                        pixels[ i * 4 ] = ( r * .393 ) + ( g *.769 ) + ( b * .189 );
                        pixels[ i * 4 + 1 ] = ( r * .349 ) + ( g *.686 ) + ( b * .168 );
                        pixels[ i * 4 + 2 ] = ( r * .272 ) + ( g *.534 ) + ( b * .131 );
                    }
 
                    context.putImageData( imageData, 0, 0 );
                };
 
 
                public.filters.contrast = function ( contrast ) {
                    var imageData = app.getImgData(),
                        pixels = imageData.data,
                        numPixels = imageData.width * imageData.height,
                        factor;
 
                    contrast || ( contrast = 100 ); // Default value
 
                    factor = ( 259 * ( contrast + 255 ) ) / ( 255 * ( 259 - contrast ) );
 
                    for ( var i = 0; i < numPixels; i++ ) {
                        var r = pixels[ i * 4 ];
                        var g = pixels[ i * 4 + 1 ];
                        var b = pixels[ i * 4 + 2 ];
 
                        pixels[ i * 4 ] = factor * ( r - 128 ) + 128;
                        pixels[ i * 4 + 1 ] = factor * ( g - 128 ) + 128;
                        pixels[ i * 4 + 2 ] = factor * ( b - 128 ) + 128;
                    }
 
                    context.putImageData( imageData, 0, 0 );
                };
 
                public.save = function () {
                    var link = window.document.createElement( 'a' ),
                        url = canvas.toDataURL(),
                        filename = 'screenshot.jpg';
 
                    link.setAttribute( 'href', url );
                    link.setAttribute( 'download', filename );
                    link.style.visibility = 'hidden';
                    window.document.body.appendChild( link );
                    link.click();
                    window.document.body.removeChild( link );
                };
 
                return public;
            } () );
        </script>
    </body>
</html>

Siguientes pasos: mejorar el rendimiento

Para no extender mucho más este artículo, hemos decidido elaborar una segunda parte donde estudiaremos el uso de Web Workers para incrementar el rendimiento de los cálculos de forma significativa. Además, aprovecharemos para darle al resto del código un lavado de cara utilizando, ya sí, una sintaxis más moderna.

Conclusión

Conocer los principios para manipular una imagen a través de sus datos, trabajándolos a nivel de píxel, es una herramienta poderosa dentro del ámbito de la edición y la tecnología front end.

Partiendo de una teoría similar a la que se ha usado tradicionalmente con otros lenguajes de programación, podemos aplicar todo ese conocimiento en Javascript para crear numerosos efectos, ya sean desde los más simples y habituales, a los más complejos y espectaculares. Podemos entender este conjunto de funcionalidades como una capa más que se le ofrece al usuario para mejorar su experiencia de uso en nuestra aplicación a través de la personalización.

Tampoco hay que perder de vista, más allá del componente estético, que esta teoría es también el punto de arranque de procesos complejos como serían el reconocimiento óptico de caracteres (OCRs como TesseractJS), la identificación facial (tracking.js), la lectura inteligente de imágenes (Google image captioning system) o el análisis de patrones (como el necesario para la detección de desnudos con Nude.js).

Si bien este artículo es solo una primera toma de contacto, esperamos que estas sean suficientes para animar al lector con sus propios proyectos.

Más:

Aún no tenemos debug!
Ningún marciano se ha comunicado.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *