Vista previa de imagen en el Navegador

El tema de este artículo será sobre una tarea bastante simple, pero que puede mejorar considerablemente la experiencia de usuario de tu sitio o aplicación web. La idea es que al solicitar al usuario una imagen para enviar al servidor (e.g. una foto personal para su perfil en línea) se pueda mostrar una vista previa…


El tema de este artículo será sobre una tarea bastante simple, pero que puede mejorar considerablemente la experiencia de usuario de tu sitio o aplicación web. La idea es que al solicitar al usuario una imagen para enviar al servidor (e.g. una foto personal para su perfil en línea) se pueda mostrar una vista previa de la imagen en pantalla, indicando las medidas de la misma (en px), o verificando su peso (en bytes), e incluso la posibilidad recortar la imagen “en vivo” si esta es muy grande.

Vamos a crear un layout de prueba en HTML5:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Vista previa antes de enviar</title>
	<script type="text/javascript" src="jquery-1.8.2.min.js"></script>
	<style type="text/css">
		.titulo{ font-size: 12pt; font-weight: bold; height: 30pt;}
		#marcoVistaPrevia{
			border: 1px solid #008000;
			width: 400px;
			height: 400px;
		}
		#vistaPrevia{
			max-width: 400px;
			max-height: 400px;
		}
	</style>

	<!-- El contenido del script más adelante -->
	<script type="text/javascript">
	</script>

</head>
<body>
	<div id='botonera'>
		<input id="archivo" type="file" accept="image/*"></input>
		<input id="cancelar" type="button" value="Cancelar"></input>
	</div>
	<div class="contenedor">
		<div class="titulo">
			<span>Vista Previa:</span>
			<span id="infoNombre">[Seleccione una imagen]</span><br/>
			<span id="infoTamaño"></span>
		</div>
		<div id="marcoVistaPrevia">
			<img id="vistaPrevia" src="" alt="" />
		</div>
	</div>
</body>
</html>

Veamos lo que tenemos: en el encabezado hay una referencia a jQuery, que usaremos más adelante, algunos estilos visuales para nuestro layout básico, y la sección del script que se encargará de la vista previa; esta la describiré más adelante.

En el cuerpo de la página tenemos una botonera con un input tipo File (para que el usuario seleccione un archivo) y un botón cancelar. El input tiene establecido el atributo accept=“image/*”; este valor lo usará el navegador como “sugerencia” para filtrar los archivos que el usuario va a seleccionar. Digo “sugerencia” porque el navegador no necesariamente verificará que el archivo sea realmente una imagen, incluso el navegador podría simplemente ignorar esta sugerencia y mostrar al usuario todos los archivos. Quedará bajo nuestra responsabilidad verificar del lado del servidor (con PHP, ASP, JSP…) si el archivo es realmente una imagen, claro que solo haremos la verificación si es realmente necesario. En nuestro caso, como lo único que queremos es mostrar una vista previa en pantalla vamos a verificar solo del lado del cliente (con javascript) si el archivo es de uno de los tipos conocidos de imagen.

Seguimos… debajo de la botonera tenemos un contenedor. En él hay un par de textos informativos, que actualizaremos al seleccionar una imagen, luego tenemos un marco de tamaño fijo (400px X 400px) y dentro de él una imagen cuyo tamaño se ajustará al espacio disponible “sin cambiar la proporción de la imagen”… esto es importante para la correcta visualización de la misma.

Una última aclaratoria antes de continuar: en HTML5 los inputs (archivo, de tipo file, y cancelar, de tipo button) pueden estar en cualquier lugar donde esté permitido colocar Expresiones de Contenido, por lo cual no los coloqué dentro de un Form. En tu caso, dependiendo de lo que quieras hacer con la imagen, tal vez tengas que colocar estos controles en un Form.

En este punto tenemos algo como esto:

Generando la vista previa

Ahora que el usuario puede seleccionar una imagen nos toca analizarla y generar la vista previa en el navegador (recuerda que queremos generar la vista previa antes de enviarla al servidor).

Este es el código que usaremos en la etiqueta script que dejamos vacia en el encabezado de la página:

//Este string contiene una imagen de 1px*1px color blanco,
//lo dividí en dos líneas debido al espacio disponible
window.imagenVacia = 'data:image/gif;base64,R0lGODlhAQABAI' +
                     'AAAAAAAP///ywAAAAAAQABAAACAUwAOw==';

window.mostrarVistaPrevia = function mostrarVistaPrevia(){

  var Archivos,
      Lector;

  //Para navegadores antiguos
  if(typeof FileReader !== "function" ){
    jQuery('#infoNombre').text('[Vista previa no disponible]');
    jQuery('#infoTamaño').text('(su navegador no soporta vista previa!)');
    return;
  }

  Archivos = jQuery('#archivo')[0].files;
  if(Archivos.length>0){

    Lector = new FileReader();
    Lector.onloadend = function(e){
      var origen,
          tipo;

      //Envía la imagen a la pantalla
      origen = e.target; //objeto FileReader

      //Prepara la información sobre la imagen
      tipo = window.obtenerTipoMIME(origen.result.substring(0, 30));

      jQuery('#infoNombre').text(Archivos[0].name + ' (Tipo: ' + tipo + ')');
      jQuery('#infoTamaño').text('Tamaño: ' + e.total + ' bytes');
      //Si el tipo de archivo es válido lo muestra,
      //sino muestra un mensaje
      if(tipo!=='image/jpeg' && tipo!=='image/png' && tipo!=='image/gif'){
        jQuery('#vistaPrevia').attr('src', window.imagenVacia);
        alert('El formato de imagen no es válido: debe seleccionar una imagen JPG, PNG o GIF.');
      }else{
        jQuery('#vistaPrevia').attr('src', origen.result);
      }

    };
    Lector.onerror = function(e){
      console.log(e)
    }
    Lector.readAsDataURL(Archivos[0]); 

  }else{
    var objeto = jQuery('#archivo');
    objeto.replaceWith(objeto.val('').clone());
    jQuery('#vistaPrevia').attr('src', window.imagenVacia);
    jQuery('#infoNombre').text('[Seleccione una imagen]');
    jQuery('#infoTamaño').text('');
  };

};

//Lee el tipo MIME de la cabecera de la imagen
window.obtenerTipoMIME = function obtenerTipoMIME(cabecera){
  return cabecera.replace(/data:([^;]+).*/, '\$1');
}

jQuery(document).ready(function(){

  //Cargamos la imagen "vacía" que actuará como Placeholder
  jQuery('#vistaPrevia').attr('src', window.imagenVacia);

  //El input del archivo lo vigilamos con un "delegado"
  jQuery('#botonera').on('change', '#archivo', function(e){
    window.mostrarVistaPrevia();
  });

  //El botón Cancelar lo vigilamos normalmente
  jQuery('#cancelar').on('click', function(e){
    var objeto = jQuery('#archivo');
    objeto.replaceWith(objeto.val('').clone());

    jQuery('#vistaPrevia').attr('src', window.imagenVacia);
    jQuery('#infoNombre').text('[Seleccione una imagen]');
    jQuery('#infoTamaño').text('');
  });

});

En el código tenemos una declaración de variable, dos funciones, y el manejador del evento ready de la página.

La primera variable contiene una cadena que es “la representación base 64 de una imagen tipo gif de 1px de color blanco”. Esta cadena la podemos usar como src de una etiqueta img. Debido a que la imagen es pequeña, preferí usar la representación de cadena en lugar de crear un archivo de imagen y vincularlo a la etiqueta img, por lo menos en este caso, porque la imagen solo la usaremos como placeholder cuando no haya una imagen seleccionada.

La función mostrarVistaPrevia será la que realice la mayor parte del trabajo. Primero verifica que el navegador tenga soporte para el objeto FileReader, ya que sin él no podremos leer la imagen en el navegador. Luego obtenemos la propiedad files del input archivo: éste es un arreglo con cero o más archivos seleccionados por el usuario.

Si el usuario seleccionó al menos un archivo, creamos un objeto FileReader (que llamaremos Lector) y asignamos dos manejadores de eventos: el primero se activará cuando termine de leer el archivo (en este ejemplo solo leeremos el primer archivo seleccionado), y el segundo se activará en caso de que haya algún error. Luego ejecutamos el método readAsDataURL del FileReader; este método intentará leer el archivo seleccionado y si lo logra disparará el evento onloadend que asignamos hace un momento, y ya estamos listos para leer la información que queremos de la imagen: primero guardamos una referencia al FileReader, que llamaremos target, y leeremos los primeros caracteres de su propiedad result (que contiene toda la imagen codificada en base 64).

La función obtenerTipoMIME, definida un poco más abajo, utiliza una expresión regular para obtener el tipo MIME de la imagen. Mostramos en pantalla el tipo MIME y tamaño de la “presunta” imagen (en bytes) y procedemos a validar que realmente sea una imagen jpg, png o gif (para el ejemplo solo probaremos esos tres tipos de imagen). Si efectivamente es una imagen, entonces usamos su representación base 64 como src de la imagen, si no lo es entonces mostramos la imagen por defecto (nuestro gif de 1px) y mostramos un mensaje al usuario.

Seguimos avanzando en el código; tenemos más abajo el manejador del evento ready del documento. En él hacemos tres tareas para inicializar nuestra página web: Primero establecemos la imagen por defecto, vigilamos el evento change del input archivo para generar la vista previa, y vigilamos el evento click del input cancelar para “limpiar” la pantalla.

Más acerca del manejo de eventos

Vamos a revisar detenidamente el fragmento de código del manejador de eventos:

jQuery(document).ready(function(){

  //Cargamos la imagen "vacía" que actuará como Placeholder
  jQuery('#vistaPrevia').attr('src', window.imagenVacia);

  //El input del archivo lo vigilamos con un "delegado"
  jQuery('#botonera').on('change', '#archivo', function(e){
      window.mostrarVistaPrevia();
  });

  //El botón Cancelar lo vigilamos normalmente
  jQuery('#cancelar').on('click', function(e){
    var objeto = jQuery('#archivo');
    objeto.replaceWith(objeto.val('').clone());

    jQuery('#vistaPrevia').attr('src', window.imagenVacia);
    jQuery('#infoNombre').text('[Seleccione una imagen]');
    jQuery('#infoTamaño').text('');
  });

});

Aquí, usamos jQuery para enlazar una función al evento ready del documento (la función será el manejador del evento ready). Este evento se disparará cuando el navegador haya cargado todo el html de la página, y es en ese momento cuando podemos ejecutar código que haga referencia a los controles o elementos de la misma; lo primero que haremos dentro de este evento será cargar la imagen por defecto.

La función on de jQuery se usa para enlazar una función a un evento de un control. Por ejemplo, al hacer clic en el botón cancelar se ejecuta la función enlazada al evento click del mismo: en esta función reemplazamos al input archivo con una copia de él mismo (es una de las dos formas seguras de “vaciar” un input de tipo file, la otra forma es usando el método reset del form) y luego cargamos la imagen por defecto y actualizamos la información en las etiquetas.

El manejador del evento change del input archivo es diferente al del botón cancelar: En este caso estamos enlazando una función al evento change de la botonera, pero le indicamos que solo ejecute la función si fue algún control con id=archivo el que disparó el evento originalmente. Este mecanismo se llama “delegación” y en general consiste en enlazar los manejadores de eventos a un contenedor en lugar de hacerlo directamente al control. En este caso, debemos usar delegación porque el control archivo es “eliminado” y creado nuevamente cada vez que se presiona cancelar.

Las medidas de la imagen

Al principio del artículo indicamos que hay un marco cuadrado de 400px, y dentro se encuentra el control img que contiene la vista previa, y que puede crecer hasta 400px de ancho y alto. Esta configuración permite que las imágenes pequeñas (de menos de 400px de ancho y alto) se muestren en su tamaño original, pero si una de las medidas sobrepasa los 400px entonces la imagen será reducida uniformemente hasta que quepa en el contenedor, manteniendo la relación de aspecto original.

Si la imagen es pequeña puedes obtener el tamaño de la imagen, en px, usando jQuery:

var ancho = jQuery('#vistaPrevia').width();
var alto = jQuery('#vistaPrevia').height();
alert('Medidas: ' + ancho + 'x' + alto);

Pero, si la imagen mide más de 400px entonces jQuery te dará el tamaño que ocupa la imagen “en pantalla”, no el verdadero tamaño en px. Para obtener las medidas reales haremos algo diferente, crearemos una imagen “en memoria” y leeremos las medidas de esa imagen, que no será afectada por las medidas de nuestra hoja de estilos. El siguiente bloque de código es nuevo y debe ir bajo la función obtenerTipoMIME:

//Obtiene las medidas de la imagen y las agrega a la
//etiqueta infoTamaño
window.obtenerMedidas = function obtenerMedidas(){
  jQuery('&lt;img/&gt;').bind('load', function(e){

    var tamaño = jQuery('#infoTamaño').text() +
                 '; Medidas: ' + this.width + 'x' + this.height;

    jQuery('#infoTamaño').text(tamaño);

  }).attr('src', jQuery('#vistaPrevia').attr('src'));
}

Y hacemos la llamada a la función obtenerMedidas desde dentro del manejador del evento onloadend, justo al verificar que el archivo es efectivamente una imagen:

Lector.onloadend = function(e){
  var origen,
      tipo;

  //Envía la imagen a la pantalla
  origen = e.target; //objeto FileReader

  //Prepara la información sobre la imagen
  tipo = window.obtenerTipoMIME(origen.result.substring(0, 30));

  jQuery('#infoNombre').text(Archivos[0].name + ' (Tipo: ' + tipo + ')');
  jQuery('#infoTamaño').text('Tamaño: ' + e.total + ' bytes');
  //Si el tipo de archivo es válido lo muestra,
  //sino muestra un mensaje
  if(tipo!=='image/jpeg' && tipo!=='image/png' && tipo!=='image/gif'){
    jQuery('#vistaPrevia').attr('src', window.imagenVacia);
    alert('El formato de imagen no es válido: debe seleccionar una imagen JPG, PNG o GIF.');
  }else{
    jQuery('#vistaPrevia').attr('src', origen.result);
    window.obtenerMedidas();
  }
};

Fijate que solo leeremos las medidas de la imagen si es realmente una imagen válida, aunque el tamaño en bytes lo leemos sin importar el tipo de archivo seleccionado.

Veamos tres fotografías con su vista previa e información de la imagen (son fotos de mi tierra Barquisimeto):

Obelisco de Barquisimeto
Obelisco de Barquisimeto
Iglesia de Santa Rosa, Barquisimeto
Iglesia de Santa Rosa, Barquisimeto
Flor de Venezuela, Barquisimeto
Flor de Venezuela, Barquisimeto

Puedes ver un ejemplo completo funcionando en jsfiddle, puedes modificarlo para hacer pruebas y al presionar Run se ejecuta; recuerda no guardar los cambios, para no llenar la base de datos de jsfiddle con pruebas.

Otras ideas

Si para nuestro sitio web tiene sentido permitir que el usuario seleccione más de una imagen (para subir al servidor varias fotos) podemos incluir en la etiqueta input el parámetro Multiple: este le indicará al navegador que el usuario puede seleccionar más de una imagen a la vez, y tendrás que modificar el código para leer todos los archivos seleccionados por el usuario mediante un bucle for (en mi código solo leemos el primer archivo de la propiedad files del input archivo). También tendrás que modificar el layout ya que no sabrás de antemano cuantas imágenes seleccionará el usuario, aunque puedes limitar el número de imágenes deteniendo el bucle cuando llegue a cierto número de iteraciones.

Otra posibilidad es dibujar esta imagen en un canvas (usando el método drawImage, mira mi articulo anterior), así podrás aplicarle algún filtro o efectos a la imagen antes de subirla, al estilo de instagram, o permitir al usuario que recorte la imagen al tamaño deseado.

Referencias

Derecho de uso

Los contenidos generados por el autor de este artículo (explicaciones, código fuente, y archivos adjuntos creados por el autor) están disponibles bajo licencia CC BY-SA 3.0, y pueden ser usados, derivados y compartidos bajo los términos indicados en la misma. Los contenidos no generados por el autor de este artículo son propiedad de sus respectivos dueños y están regidos por las licencias que estos hayan dispuesto.

Cita del día

You get the best out of others when you give the best of yourself.

Harvey S. Firestone
,

11 respuestas a “Vista previa de imagen en el Navegador”

  1. Me surge un problema, copie el código html en dos partes de mi código, obviamente importe el js y puse el css en mi css general, el problema es que solo funciona el primero de los dos, osea el bloque de codigo que esta primero, vuelvo a repetir que puse el html que hiciste en mi codigo, pero en dos partes y solo funciona el primero a las mil maravillas mas no en el segundo, imagino que es por algo del js. Si podrias ayudarme.

  2. Saludos Luis, según entiendo tienes más de un input de tipo file. Probablemente el problema esté en jQuery('#botonera').on('change'...

    Verás, aquí estoy enlazando una función al evento change del imput con id=botonera. Si tienes un segundo input debería tener un ID diferente. Además si tienes más de un elemento img para mostrar las vistas previas entonces cada uno tendrá diferente ID; así tendrás también que modificar la función mostrarVistaPrevia para que ésta sepa cual de las vistas previas fue la que cambió (podrías pasarle un parámetro con el Id del input, o un índice entero).

    • Saludos sergiors! tienes dos vías: lo mejor sería quitar el «border» del css de marco y ponérselo a la imagen (probablemente quieras también quitar el width y height del marco, depende de tu layout). La otra forma es ponerle «display:inline» al marco (y un min-width y min-height a la image, para que el marco no «colapse» cuando no haya imagen seleccionada). Has la prueba y comenta por aquí como te fue!

  3. Hola que tal ahorita estoy desarrollando un sistema, y lo que quiero hacer es esa previsualización para luego mandarlo a la base de datos la ruta de la imagen y mover la imagen almacenada al servidor, ¿Como seria eso en php?

  4. hola buenas, si quiero realizar la carga de varias imágenes, a parte de poner en el input file lo de multiple=»True» el for que dices que hay que poner para que lea todas las imágenes y las precargue en el layout? gracias de antemano

Soluciones claras y simples



Ing. Industrial, dedicado a la programación en ASP.NET+VB, SQL y Javascript+AJAX; y un poco de Android, Kotling, y Unity 🙂
Valencia, Venezuela



¿QUIERES APOYARME?

¿Te ha sido de ayuda alguno de mis artículos? Generar contenido técnico requiere de tiempo y esfuerzo. Con tu colaboración me puedes ayudar a mantener mi blog activo y actualizado. Si quieres y puedes apoyarme has clic aquí:

https://paypal.me/roimergarcia


ENTRADAS RECIENTES