HTML5 Apis: File API

¡Muy buenas! Vamos a empezar una serie de posts (que como digo siempre, a ver donde nos llevan) sobre las APIs de HTML5, dado que hay muchas (algunas más conocidas que otras). La que veremos en este post es File API que dicho rápidamente nos permite leer ficheros locales usando javascript.

Si al leer que ahora podemos leer ficheros desde javascript se te han puesto los pelos como escarpias pensando en los posibles agujeros de seguridad, tranquilo: no hay forma alguna de leer un fichero a través de su ruta. Siempre se requiere que sea el usuario el que inicie la acción y abra explícitamente el fichero.

Leyendo datos de un fichero

Lo primero que necesitamos para poder acceder al contenido de un fichero es que el usuario lo abra. ¿Y que mecanismo tenemos en HTML para permitir al usuario seleccionar uno o varios ficheros? Exacto! El feote <input type=”file” /> y eso es lo primero que necesitamos.

    1 <!DOCTYPE html>

    2 

    3 <html>

    4     <head>

    5         <title>title</title>

    6     </head>

    7     <body>

    8         <div>

    9             Selecciona un fichero: <br />

   10             <input type=»file» value=»Leer» id=»fichero»/> 

   11         </div>

   12         <script type=»text/javascript»>

   13             function ficheroSeleccionado(evt) {

   14                 var ficheros = evt.target.files;

   15                 // Tan solo procesaremos el primer fichero

   16                 var fichero = ficheros[0];

   17             }

   18             document.getElementById(«fichero»).addEventListener(‘change’, ficheroSeleccionado, false);

   19         </script>

   20     </body>

   21 </html>

Si ejecuto eso mismo en Chrome, y pongo un breakpoint en la línea 17 veo lo siguiente:

image

Podemos ver como efectivamente la propiedad files del <input> me ha devuelto una FileList y el primer elemento es un File con la información del archivo seleccionado. ¡Bien!

Una vez tenemos un objeto File ya podemos leerlo, para ello debemos instanciar un FileReader y llamar a algunos de sus métodos:

  • readAsBinaryString: para leerlo de forma binaria . Ese método devuelve una cadena con el contenido binario. Si te parece raro que se reciba una cadena con el contenido en binario, decirte que a los que crearon la API les debió parecer lo mismo (léete la PD al final del post).
  • readAsText: para leerlo como texto
  • readAsDataURL: para leerlo como “data-url” (eso es ideal para previews de imágenes).

Vamos a ver un ejemplo rapidísimo: vamos a crear un visor hexadecimal. Para ello añadimos dos <div>s, uno de los cuales contendrá los códigos hexadecimales de cada byte del archivo y otro contendrá el carácter ASCII imprimible asociado:

    1         <style>

    2             .terminal {font-family: courier new}

    3             #raw {float: left}

    4             #rawtext {float:right}

    5         </style>

    1         <div id=»raw» class=»terminal»></div>

    2         <div id=»rawtext» class=»terminal» ></div>

Y ahora usaremos readAsBinaryString para obtener el contenido binario del archivo.

Cuando usemos las funciones de FileReader debemos tener presente que esas NO devuelven ningún resultado, si no que nos lanzan un evento cuando la lectura ha finalizado, y entonces podemos consultar el valor de la propiedad result del FileReader. Para ello modificamos la función ficheroSeleccionado para que quede tal y como sigue:

    1 function ficheroSeleccionado(evt) {

    2     var ficheros = evt.target.files;

    3     // Tan solo procesaremos el primer fichero

    4     var fichero = ficheros[0];

    5     var reader = new FileReader();

    6 

    7     reader.onloadend = function(ievt) {

    8         if (ievt.target.readyState == FileReader.DONE) {

    9             mostrarResultado(ievt.target.result);

   10         }

   11     };

   12 

   13     reader.readAsBinaryString(fichero);

   14 }

Fijaos que asignamos una función gestora al evento loadend, que nos lanza el FileReader para indicar que la lectura ha finalizado. En esta función gestora nos limitamos a asegurarnos que realmente la lectura no ha dado error y si es el caso llamamos a mostrarResultado que será la función que rellenerá los dos <div>s:

    1 function mostrarResultado(resultado) {

    2     var raw = document.getElementById(«raw»);

    3     var rawtext = document.getElementById(«rawtext»);

    4     var shtmlraw = «»;

    5     var shtmltext = «»;

    6     for (var idx = 0; idx < resultado.length; idx++) {

    7         var ascii = resultado.charCodeAt(idx);

    8         var codigo = ascii.toString(16);

    9         if (codigo.length < 2) codigo = «0» + codigo;

   10         shtmlraw += codigo + «&nbsp;»;

   11         shtmltext += ascii >= 32 && ascii <= 127 ? «&#» + ascii : «.»;

   12         if ((idx + 1) % 8 == 0 && (idx + 1) % 24 != 0) {

   13             shtmlraw += «&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;»;

   14         }

   15         if ((idx+1) % 24 == 0) {

   16             shtmlraw += «<br />»;

   17             shtmltext += «<br />»;

   18         }

   19     }

   20 

   21     raw.innerHTML = shtmlraw;

   22     rawtext.innerHTML = shtmltext;

   23 }

No hay mucho que contar sobre esa función, es javascript puro y duro 🙂 Un consejo por si alguna vez usáis la propiedad innerHTML para asignar código HTML a un elemento del DOM: es muuuuuuuuy lenta. Así que es mejor ir creando el código en una cadena y usar innerHTML una sola vez al final.

¡Y listos! Ya puedo seleccionar un fichero cualquiera y ver su contenido binario:

image

Ahhh… que nostalgia de aquellos tiempos de MS-DOS, con aquellos editores hexadecimales tan bonitos, eh??? Pues bueno, aquí tenéis una buena imitación, eso sí solo lectura, que File API no permite modificar en ningún caso el contenido de ningún archivo!

Previsualizando imágenes

Bueno… ya puestos vamos a añadir un poco de funcionalidad a nuestro editor hexadecimal. Si el archivo seleccionado es una imagen vamos a visualizarla como tal (junto con su volcado hexadecimal).

Y es que File API nos permite de forma muy fácil crear una previsualización de una imagen, gracias al método readAsDataURL. Ya hablé hace tiempo en este blog de las data url así que no me repetiré ahora, simplemente para resumir: el atributo src de un <img> puede ser una URI especial (que empieza por data:) y que tiene el contenido en Base64 de la imagen a mostrar.

Pues bien, eso es precisamente lo que nos devuelve readAsDataURL. Así para previsualizar una imagen basta con asignar su atributo src al valor leído por el FileReader (en el evento loadend por supuesto). Veamos como queda el código completo.

En este caso el código queda un poco más liado porque lo que debemos hacer ahora es:

  1. Si el fichero es una imagen debemos
    1. Leerlo usando readAsDataURL y previsualizarlo
    2. Leerlo de nuevo usando readAsBinaryString y previsualizarlo
  2. Si el fichero NO es una imagen debemos
    1. Leerlo usando readAsBinaryString y previsualizarlo

Para ello modificamos primero la función ficheroSeleccionado para saber si el fichero es una imagen o no y actuar en consecuencia:

    1 function ficheroSeleccionado(evt) {

    2     var ficheros = evt.target.files;

    3     // Tan solo procesaremos el primer fichero

    4     var fichero = ficheros[0];

    5     var reader = new FileReader();

    6 

    7     if (fichero.type.match(‘image.*’)) {

    8         leerImagen(reader, fichero);

    9     }

   10     else {

   11         leerBinario(reader, fichero);

   12     }

   13 

   14 }

El método leerBinario es simplemente el código de lectura que teniamos antes en ficheroSeleccionado:

    1 function leerBinario(reader, fichero) {

    2     reader.onloadend = function (ievt) {

    3         if (ievt.target.readyState == FileReader.DONE) {

    4             mostrarResultado(ievt.target.result);

    5         }

    6     };

    7 

    8     reader.readAsBinaryString(fichero);

    9 }

El método mostrarResultado por supuesto es el mismo de antes 😉

Vayamos ahora a por el método leerImagen. Este simplemente leerá usando readAsDataURL y luego llamará a leerBinario:

    1 function leerImagen(reader, fichero) {

    2     reader.onloadend = function (ievt) {

    3         if (ievt.target.readyState == FileReader.DONE) {

    4             previsualizarImagen(ievt.target);

    5             leerBinario(reader, fichero);

    6         }

    7     };

    8     reader.readAsDataURL(fichero);

    9 }

Y para completar la ecuación tan solo nos queda el método previsualizarImagen:

    1 function previsualizarImagen(filereader) {

    2     var datauri = filereader.result;

    3     var image = document.getElementById(«preview»);

    4     image.src = datauri;

    5     image.style.visibility = «visible»;

    6 }

Fijaos que se limita a poner en el atributo src de una imagen el valor leído por el FileReader usando readAsDataURL. Y eso es lo que vemos si seleccionamos una imagen:

image

Fijaos como podemos ver la imagen, sin que esa haya subido en ningún momento al servidor.

¡Y listos! Con eso ya hemos visto lo básico de File API. Hay más, pero mejor lo dejamos para otro post de esta serie 😉

PD: Notas sobre soporte de los navegadores.

El código de este post lo he probado con Chrome (a la hora de escribirlo era la versión 23.0, cuando le leáis a saber cual es) y con IE10. Ambos soportan File API (al igual que el resto de los navegadores importantes).

Peeeeeero el método readAsBinaryString NO está soportado en IE10. Pero, que quede claro, no es un incumplimiento del estándard: El estandard de la W3C no define un método readAsBinaryString (lo podeis ver en http://www.w3.org/TR/FileAPI/#dfn-filereader). Existió tiempo atrás pero se marcó como obsoleto (y en la versión final ha desaparecido). Chrome lo mantiene (creo que FF también) pero IE10 es más estricto con el cumplimiento del estandard y no lo hace.

Eso es típico cuando usamos HTML5: algunas de esas APIs están vivas o se han terminado hace muy poco y a veces es fácil caer en métodos obsoletos o ya desparecidos.

PD2: Vale… muy bien ¿pero como acceder al contenido binario usando la última especificación del estandard?

¡Que nadie se alarme! Es muy sencillo: debemos usar el método readAsArrayBuffer() en lugar de readAsBinaryString() que nos devuelve un ArrayBuffer que contiene los valores. Veamoslo.

El cambio a leerBinario es trivial:

    1 function leerBinario(reader, fichero) {

    2     reader.onloadend = function (ievt) {

    3         if (ievt.target.readyState == FileReader.DONE) {

    4             mostrarResultado(ievt.target.result);

    5         }

    6     };

    7 

    8     reader.readAsArrayBuffer(fichero);

    9 }

Y será mostrarResultado el que ahora en lugar de recibir una cadena con el contenido en binario recibe un ArrayBuffer. El tema es que los ArrayBuffer no se pueden consultar directamente. En su lugar debe usarse una vista por encima del ArrayBuffer. En nuestro caso dado que queremos leer byte a byte usaremos un Uint8Array.

El código de mostrarResultado es idéntico al que ya teníamos con la excepción del principio:

    1 function mostrarResultado(resultado) {

    2     var raw = document.getElementById(«raw»);

    3     var rawtext = document.getElementById(«rawtext»);

    4     var shtmlraw = «»;

    5     var shtmltext = «»;

    6     var view = new Uint8Array(resultado);

    7     for (var idx = 0; idx < view.length; idx++) {

    8         var ascii = view[idx];

El resto del código es idéntico. Fijaos como creamos el Uint8Array por encima del ArrayBuffer devuelto y luego ya leemos byte a byte. Dado que ahora view es un array ya de bytes y no una cadena como antes, ya no necesitamos usar charCodeAt() para obtener el código ascii (valor del byte) asociado.

Ahora sí que el código debería funcionar tanto en IE10, como en Chrome, como en FF como en el resto de navegadores importantes!

4 comentarios sobre “HTML5 Apis: File API”

Deja un comentario

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