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('<img/>').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):
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
- Definición de la interfaz del objeto FileReader (material técnico de la W3C)
- Información detallada del uso de Expresiones Regulares en JavaScript
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”
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.
Saludos Luis, según entiendo tienes más de un
input
de tipofile
. Probablemente el problema esté enjQuery('#botonera').on('change'...
Verás, aquí estoy enlazando una función al evento
change
del imput conid=botonera
. Si tienes un segundoinput
debería tener un ID diferente. Además si tienes más de un elementoimg
para mostrar las vistas previas entonces cada uno tendrá diferente ID; así tendrás también que modificar la funciónmostrarVistaPrevia
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).Habrá alguna manera para que el marco se ajuste al tamaño de la imagen ?
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!
[…] cuando el usuario selecciona una imagen y genera una vista previa a partir de ella (en mi artículo Vista previa de imagen en el navegador puedes ver más información al respecto). En resumen, este controlador leerá la imagen […]
ola amigo no me funciona en chrome
Hola Will, ¿Que es lo que no te funciona? ¿Te sale al menos el nombre y tamaño de la imagen? Si me describes el error tal vez pueda ayudarte 🙂
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?
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
Reblogueó esto en Emmandeby comentado:
interesante
Muchísimas gracias por este excelentes blog.. un saludo grande desde Paraguay