Enviar imágenes desde HTML 5 canvas a servidor

Hace prácticamente un mes que no he tenido tiempo de pararme a escribir y creo que vuelvo con un tema que me parece muy interesante y potente 🙂 A día de hoy, cuando tenemos que trabajar con imágenes, las cuales necesitaban un posterior tratamiento en cuanto a tamaño, filtros, etcétera, hacíamos uso de Flash o del lado de servidor para hacer este tratamiento. Esto suponía que si la imagen original ocupaba 2MB teníamos que hacer esa subida para poder tratarla a posteriori y desechar (o no) la imagen original.

En este post os quiero mostrar cómo es posible hacer uso de varias APIs de HTML 5 que nos van a hacer la vida más fácil en este aspecto.

¿Cuál es el objetivo?

El objetivo real siempre ha sido el mismo: almacenar en servidor el resultado final de una imagen que yo proporciono sin tener que subir nada más. Para conseguirlo, haremos el tratamiento que necesitemos a nuestra imagen en el lado del cliente y sólo cuando esté lista enviaremos la petición al servidor con el resultado final. Además podemos tener dos aproximaciones: que dicho envío se tenga que realizar de forma síncrona o asíncrona, por lo que veremos los dos 😀

¿Por dónde empiezo?

En primer lugar, he creado el siguiente código HTML:

<!DOCTYPE html>
<html>
<head></head>
<body>
    <input type="file" id="pic" name="pic" />
    <canvas id="preview"></canvas>
    <div id="actions">
        <form action="/Home/UploadImage" method="POST">
            <input type="text" id="imageName" name="imageName" />
            <input type="hidden" id="contentType" name="contentType" />
            <input type="hidden" id="imageData" name="imageData" />
            <input id="btnSave" type="submit" value="Save" />
        </form>
    </div>
    <div id="result"></div>
    <script src="~/App/boot.js"></script>
</body>
</html>

El primero de los input lo vamos a utilizar para recuperar el archivo que queremos subir. A través del canvas mostraremos la imagen con los cambios realizados antes de enviar al servidor. Por último, en el caso de que necesitemos hacer una petición síncrona se ha creado un formulario con la acción donde se va a gestionar la información y una serie de campos que completaremos desde el lado de JavaScript.

Código JavaScript

En primer lugar, estoy haciendo uso de una pequeña función para reducir el código cada vez que solicito un elemento del DOM (manías mías :))

function id(elementId) {
    return document.getElementById(elementId);
}

Cuando la página esté lista voy a configurar los siguientes valores:

window.onload = function () {
    //constants
    var MAX_WIDHT = 200,
        MAX_HEIGHT = 200;
    var URL = window.webkitURL || window.URL;
    var inputFile = id('pic');
}

Con ellos sabré cuál es mi tamaño máximo para mi imagen, con el objetivo de redimensionarla, utilizaré window.URL para crear URLs de objetos y recuperaré el elemento input de tipo file al que agregaré el siguiente manejador:

inputFile.addEventListener('change', function (event) {
    var file = event.target.files[0];
    //elements
    var canvas = id('preview'),
        ctx = canvas.getContext('2d'),
        url = URL.createObjectURL(file);
    var img = new Image();
    img.onload = function () {
        var width = img.width,
            height = img.height;
        //resize
        if (width > height) {
            if (width > MAX_WIDHT) {
                height *= MAX_WIDHT / width;
                width = MAX_WIDHT;
            }
            else {
                if (height > MAX_HEIGHT) {
                    width *= MAX_HEIGHT / height;
                    height = MAX_HEIGHT;
                }
            }
        }
        canvas.width = width;
        canvas.height = height;
        ctx.drawImage(img, 0, 0, width, height);
        //Form data (POST)
        console.log(file);
        //name
        var imageName = id('imageName');
        imageName.value = file.name;
        //contentType
        var contentType = id('contentType');
        contentType.value = file.type;
        //image data
        var imageData = id('imageData'),
            dataUrl = id('preview').toDataURL('image/png').replace('data:image/png;base64,', '');
        imageData.value = dataUrl;
    };
    img.src = url;
});

Cuando selecciono un archivo recupero el mismo a través de event.target.files[0], recupero el contexto del canvas y creo una URL de mi archivo con la que poder trabajar. A partir de este momento voy a utilizar un objeto Image para redimensionar su tamaño al máximo que he establecido. Una vez tenga las medidas, se las asociaré al elemento canvas para que se ajuste al mismo y llamaré al método drawImage con esos valores. Llegados a este punto tendré la vista preliminar de mi imagen 🙂 Por último, para el método síncrono, basta con asociar los valores obtenidos a los elementos de tipo hidden que tengo en mi formulario, con el objetivo de que sean enviados cuando haga clic en el botón submit incluido en él. Si os fijáis, la imagen se manda en base 64 gracias al método toDataURL que nos ofrece canvas.

¿Cómo recupero esta información en servidor?

En este ejemplo estoy utilizando ASP.NET MVC en la parte de Back end. Por ello, el controlador Home tiene el siguiente aspecto:

using System;
using System.Web.Mvc;
namespace CanvasToServer.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
        [HttpPost]
        public ActionResult UploadImage(string imageName, string contentType, string imageData)
        {
            byte[] data = Convert.FromBase64String(imageData);
            return File(data, contentType, imageName);
        }
    }
}

En la acción UploadImage tengo como argumentos los valores con el mismo nombre que los hidden asociados en mi formulario HTML. Como conozco cuál es el formato en que llega la información, basta con convertir de base 64 a un array de bytes y con él realizar las operaciones oportunas. En mi caso, lo único que hago es devolver de nuevo al cliente la imagen que acabo de subir transformada en forma de archivo, con el objetivo de poder comprobar que se ha generado correctamente.

Subida de la imagen en canvas a través de AJAX

En el caso de que necesite hacer la petición a través de AJAX, basta con reemplazar el siguiente código por el que le sigue:

//Form data (POST)
//console.log(file);
////name
//var imageName = id('imageName');
//imageName.value = file.name;
////contentType
//var contentType = id('contentType');
//contentType.value = file.type;
////image data
//var imageData = id('imageData'),
var dataUrl = id('preview').toDataURL('image/png').replace('data:image/png;base64,', '');
//imageData.value = dataUrl;
//(AJAX)
id('btnSave').addEventListener('click', function (e) {
    e.preventDefault();
    console.log("AJAX way");
    var formData = new FormData();
    formData.append('imageName', file.name);
    formData.append('contentType', file.type);
    formData.append('imageData', dataUrl);
    $.ajax({
        type: 'POST',
        url: '/Home/UploadImage',
        data: formData,
        processData: false,
        contentType: false,
        success: function (result) {
            var imgElement = document.createElement('img');
            imgElement.src = 'data:image/png;base64,' + result;
            id('result').appendChild(imgElement);
        }
    });
});
En este caso tampoco sería necesario el formulario y sus hidden, ya que usaremos FormData para enviar la información.

En la segunda opción, manejamos el evento click del botón submit y creamos un objeto de tipo FormData. Este objeto se trata de una mejora de XMLHttpRequest Level 2 donde podemos crear una colección de elementos clave valor que representarán los campos y valores de un formulario. Utiliza el mismo formato que si usáramos «multipart/form-data». Más información.
La información es la misma que almacenábamos en los campos hidden del formulario. En la petición AJAX utilizamos la misma para el campo data, indicamos la URL de la acción que procesará la petición y por último debemos indicar las opciones processData: false y contentType: false. Si la petición resulta satisfactoria, lo que nos va a devolver es la misma imagen que hemos subido y la inyectaremos en el DOM (únicamente nos sirve como test, ya que no tiene mucho sentido ;)).

En el lado del servidor he modificado la acción para que sepa diferenciar las peticiones AJAX y devolver en este caso lo mismo que le llegó por parámetro:

using System;
using System.Web.Mvc;
namespace CanvasToServer.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
        [HttpPost]
        public ActionResult UploadImage(string imageName, string contentType, string imageData)
        {
            byte[] data = Convert.FromBase64String(imageData);
            if (Request.IsAjaxRequest())
                return Content(imageData); //useless - only for testing
            return File(data, contentType, imageName);
        }
    }
}

Espero que haya sido de utilidad 🙂

¡Saludos!