Reducir una imagen antes de subirla via Canvas

Normalmente a la hora de subir una imagen a un servidor o hosting mediante un formulario se utilizan los input de tipo file.
¿Qué sucede si la imagen que queremos subir no debe superar un cierto tamaño?

En la mayoría de casos lo que se hace es enviar esa imagen a través del input y una vez se ha cargado en el servidor, éste comprueba si tiene un tamaño aceptado y si no lo tiene aborta el proceso y la destruye. Cuantas veces no habéis visto el típico “Subiendo” en la barra del navegador y una vez acaba dice algo como “Archivo demasiado grande”.

Con Canvas podemos reducir la imagen con el fin de reducir el tamaño antes de subirla. Obviamente perderemos calidad, pero nos ahorrará poner un mensaje diciendo que no se admiten imágenes > 1 MB y que luego nos contacten usuarios, que no se molestan en leer, diciendo que intentan subir un TIFF de 50MB y les peta la web (/sight).

La idea es fijar un tamaño fijo de píxeles para el ancho (Nx) y el alto (Ny) de la imagen y que al reducirla esta no pueda sobrepasar nunca esos tamaños. Es más, para asegurarnos, podríamos hacer que Nx y Ny fueran iguales y definir N = Nx = Ny. Esto aseguraría que cualquier imagen que tuviera longitud y altura por debajo de N, ocuparía siempre menos que su imagen de número de píxeles NxN.

Y si por lo que fuera las imágenes tuvieran que ocupar un tamaño considerable siempre se puede prescindir de la subida por defecto por input file y utilizar objetos Blob de Canvas para controlar la transferencia de subida.

Suponiendo que tenemos una input file que utilizamos para coger las imágenes:

<input id="fbBrowseBtn" type="file" name="photo[]" multiple>

Con el siguiente código de Javascript podríamos cojer la imagen, reducirla, enviarla y controlar el progreso una vez haya finalizado la acción:

var files = document.getElementById('fbBrowseBtn').files;
resizeAndUpload(files[0], uploaded, progressBar);
 
function resizeAndUpload(file, callback, progress)
{
	var reader = new FileReader();
	reader.onloadend = function() {
		var tempImg = new Image();
		tempImg.onload = function() {
			// Comprobamos con el aspect cómo será la reducción
			// MAX_IMAGE_SIZE_PROCESS es la N que definimos como máxima
			var MAX_WIDTH = MAX_IMAGE_SIZE_PROCESS;
			var MAX_HEIGHT = MAX_IMAGE_SIZE_PROCESS;
			var tempW = tempImg.width;
			var tempH = tempImg.height;
			if (tempW > tempH) {
				if (tempW > MAX_WIDTH) {
					tempH *= MAX_WIDTH / tempW;
					tempW = MAX_WIDTH;
				}
			} else {
				if (tempH > MAX_HEIGHT) {
					tempW *= MAX_HEIGHT / tempH;
					tempH = MAX_HEIGHT;
				}
			}
			// Creamos un canvas para la imagen reducida y la dibujamos
			var resizedCanvas = document.createElement('canvas');
			resizedCanvas.width = tempW;
			resizedCanvas.height = tempH;
			var ctx = resizedCanvas.getContext("2d");
			ctx.drawImage(this, 0, 0, tempW, tempH);
			var dataURL = resizedCanvas.toDataURL("image/jpeg");
 
			// Pasamos la dataURL que nos devuelve Canvas a objeto Blob
			// Envíamos por Ajax el objeto Blob
			// Cogiendo el valor de photo (nombre del input file)
			var file = dataURLtoBlob(dataURL);
			var fd = new FormData();
			fd.append("photo", file);
 
			$.ajax({
				url: <<url del endpoint que se encarga de la subida>>,
				type: "POST",
				data: fd,
				processData: false,
				contentType: false,
				dataType: 'json',
				xhr: function() {
					var xhr = new window.XMLHttpRequest();
					xhr.upload.addEventListener("progress", function(evt) {
						if (evt.lengthComputable) {
							// Calculando el porcentaje de todo el proceso 
							var percentComplete = evt.loaded / evt.total;
							progress(percentComplete * PERCENT_UPLOAD * 0.01);
						}
					}, false);
					return xhr;
				}
			}).done(function(respond) {
				// Una vez ha acabado la subida
				callback(respond);
			});
		};
		tempImg.src = reader.result;
	}
	reader.readAsDataURL(file);
}
 
function dataURLtoBlob(dataURL)
{
	// Decodifica dataURL
	var binary = atob(dataURL.split(',')[1]);
	// Se transfiere a un array de 8-bit unsigned
	var array = [];
	var length = binary.length;
	for(var i = 0; i < length; i++) {
		array.push(binary.charCodeAt(i));
	}
	// Retorna el objeto Blob
	return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
}
 
function uploaded(response)
{
	// Código siguiente a la subida
}
 
function progressBar(percent)
{
	// Código durante la subida
}

El método controlador del endpoint sería algo así:

$data = array();
$data['success'] = false;
// Miramos que efectivamente sea un imagen
$size = getimagesize($_FILES['photo']['tmp_name']);
if($size) {
	if(	(
		$_FILES['photo']['type'] == 'image/gif' || 
		$_FILES['photo']['type'] == 'image/jpeg' || 
		$_FILES['photo']['type'] == 'image/jpg' || 
		$_FILES['photo']['type'] == 'image/pjpeg' || 
		$_FILES['photo']['type'] == 'image/png' || 
		$_FILES['photo']['type'] == 'image/x-png')) {
			// Es una imagen, la guardamos cogiendo los datos de la key 'photo' de $_FILES
			$newFile = "path definitiva donde se guardará la imagen";
			if (move_uploaded_file($_FILES['photo']['tmp_name'], $newFile)) {
				// Todo ha ido bien, devolvemos la ubicación de la imagen.
				// Marcamos la respuesta como buena
				$data['success'] = true;
				$data['image'] = $newFile;
				$data['msg'] = lang('upload_success');
			} else {
				// Error transfiriendo la imagen a la ubicación
				$data['msg'] = lang('upload_error');
			}
	} else {
		// No se trata de una imagen habitual
		$data['msg'] = lang('upload_error_extension');
	}
 
} else {
	// No es una imagen
	$data['msg'] = lang('upload_error_noimage');
}
echo json_endode($data);

Ambos casos tuve que aplicarlos en una nueva página que hice por mera diversión: Let’s Pixel.

Etiquedado como

10 respuestas a Reducir una imagen antes de subirla via Canvas

  1. Elgar

    Muchas gracias por el aporte, después de tanto buscar, por fin una grandiosa solución. Puse a prueba el código y funciona de maravilla. Una imagen de 4,5 Mb que no podía subir al servidor, pues me daba error por el gran tamaño, pues con este código sube sin problema ya que reduce el tamaño en el cliente. Gracias de verdad gracias amigo!!!

  2. De nada ;)

  3. Julián

    Hola Jonas,

    Lo he estado probando y no me acaba de funcionar, me da un fallo en PERCENT_UPLOAD, supuestamente es una variable? no la encuentro definida en ninguna parte del código y no he podido deducir para que sirve.

    Un poco de ayuda?

    Muchas gracias por el código, si consigo llegar a aplicarlo puede ser buenísimo.

    Saludos!!

  4. Hola.

    Tienes razón, es una variable que utilicé para pasar un porcentaje parcial a la función “progress”. Lo hacía así, porque aparte de subir la imagen, despúes se realizaban otras operaciones que llamaban a progress y, por consiguiente, aumentaban el porcentaje.

    Sí sólo vas a utilizar progress con el porcentaje de la subida, puedes utilizar:
    progress(percentComplete);

    y en progress trabajar con esos valores.

  5. Julián

    Buenos días Jonás

    Muchísimas gracias por tu respuesta.
    La verdad es que conseguí que la foto cambiara de tamaño en el navegador cliente pero ahora me queda la parte de subirla al servidor, como la cargaba a través de un formulario al enviarla mediante submit me la subía con el mismo tamaño de la original y me obviaba la temporal con el nuevo tamaño :-)

    La parte en php ya la tenía definida y no la cambié. Pero creo que con tu código en php más o menos lo tengo en parte solucionado lo que quiero hacer.

    Tienes mi correo por si necesitas cualquier cosa :-)

    De nuevo, muchas gracias!!!

  6. Hola amigo, muy bueno tu articulo, pero resulta que hice una modificacion para subir hasta 10 imagenes al mismo tiempo
    Entonces yo hice un ciclo que llama esta funcion
    resizeAndUpload(files[0], uploaded, progressBar);
    en lugar de files[0], uso files[i], dependiendo de la iteraccion.

    Entonces me funciona perfectamente, el detalle esta, en que la memoria de la maquina para el proceso de Firefox mozilla, se pone muy pesado, hasta consumir 2 GB de ram para 10 imagenes. Entonces quisiera saber como pudiera hacer para optimizar este consumo, es decir, poder limpiar la memoria, a medida que sube cada imagen. Cuando hago los llamados con un ciclo, entonces el genera 10 Hilos, donde cada hilo la redimensiona y la envia al server. Pero yo necesito es algo como que redimensione una imagen, la envie, luego limpie memoria y siga con la segunda.

    He intentado poner variables en null, en diferentes sitios del codigo, pero no logro que mejore lo de la memoria. Ya que cada uno de los 10 hilos pues usa su propia memora del cliente. Y el problema es mayor si seleccionan 20 o mas imagenes.

    Espero su pronta respuesta, agradeciendo de antemano

  7. Jose Collazos

    Jonas

    Estoy mirando tu post y me parece muy interesante, y puede ser lo que este buscando, pero soy muy nuevo en esto de la programación, soy mas vale diseñador, aficionado (empírico) a la programación, pero algunas cosas no las agarro de una. Quería preguntarte si no tienes una pagina ejemplo. Es que no la acabo de agarrar por completo.

    Un saludo desde Colombia.

  8. Hola Jonas!

    Pensaba que no lo lograría pero gracias a tu post he podido lidiar con un problema de mi hosting: no podía redimensionar en el servidor mediante PHP ya que me lanzada un error para imágenes de tamaño grande (px.) o muy pesadas. He tenido que adaptarlo pero funciona estupendamente.

    MILLONES DE GRACIAS

  9. mef

    no me funciona el ajax.. porfavor ayudame

  10. Alejandro Kennedy

    Excelente, funciona muy bien, gracias por el aporte me ayudaste bastante

Deja un comentario

Tu dirección de correo electrónico no será publicada.