HTML5 Pre-cambiar el tamaño de las imágenes antes de subirlas

Aquí hay un rascador de fideos.

Teniendo en cuenta que tenemos almacenamiento local HTML5 y xhr v2 y qué no. Me preguntaba si alguien podría encontrar un ejemplo de trabajo o simplemente darme un sí o un no para esta pregunta:

¿Es posible dimensionar previamente una imagen utilizando el nuevo almacenamiento local (o lo que sea), de modo que un usuario que no tenga ni idea de cambiar el tamaño de una imagen pueda arrastrar su imagen de 10mb a mi sitio web, cambiar su tamaño usando el nuevo almacenamiento local y ENTONCES subirlo en el tamaño más pequeño.

Sé muy bien que puedes hacerlo con Flash, applets de Java, X activa … La pregunta es si puedes hacerlo con Javascript + Html5.

Esperando la respuesta en este.

Ta por ahora.

Sí, use File API , luego puede procesar las imágenes con el elemento canvas .

Esta publicación de blog de Mozilla Hacks lo guía durante la mayor parte del proceso. Para referencia, aquí está el código fuente ensamblado de la publicación del blog:

// from an input element var filesToUpload = input.files; var file = filesToUpload[0]; var img = document.createElement("img"); var reader = new FileReader(); reader.onload = function(e) {img.src = e.target.result} reader.readAsDataURL(file); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); var MAX_WIDTH = 800; var MAX_HEIGHT = 600; var width = img.width; var height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); var dataurl = canvas.toDataURL("image/png"); //Post dataurl to the server with AJAX 

Abordé este problema hace unos años y cargué mi solución a github como https://github.com/rossturner/HTML5-ImageUploader

La respuesta de robertc usa la solución propuesta en la publicación de blog Mozilla Hacks , sin embargo, encontré que esto dio una calidad de imagen muy pobre al cambiar el tamaño a una escala que no era 2: 1 (o un múltiplo de la misma). Comencé a experimentar con diferentes algoritmos de cambio de tamaño de la imagen, aunque la mayoría terminó siendo bastante lenta o tampoco fue de gran calidad.

Finalmente se me ocurrió una solución que creo que se ejecuta rápidamente y tiene un rendimiento bastante bueno también, ya que la solución de Mozilla de copiar de 1 canvas a otro funciona rápidamente y sin pérdida de calidad de imagen en una proporción de 2: 1, dado un objective de x píxeles de ancho y píxeles de alto, uso este método de cambio de tamaño de canvas hasta que la imagen esté entre x y 2 x , y y y 2 y . En este punto, me vuelvo a cambiar el tamaño algorítmico de la imagen para el “paso” final de redimensionamiento hasta el tamaño objective. Después de probar varios algoritmos diferentes, establecí la interpolación bilineal tomada de un blog que ya no está en línea, pero accesible a través del Archivo de Internet , que da buenos resultados, aquí está el código aplicable:

 ImageUploader.prototype.scaleImage = function(img, completionCallback) { var canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); while (canvas.width >= (2 * this.config.maxWidth)) { canvas = this.getHalfScaleCanvas(canvas); } if (canvas.width > this.config.maxWidth) { canvas = this.scaleCanvasWithAlgorithm(canvas); } var imageData = canvas.toDataURL('image/jpeg', this.config.quality); this.performUpload(imageData, completionCallback); }; ImageUploader.prototype.scaleCanvasWithAlgorithm = function(canvas) { var scaledCanvas = document.createElement('canvas'); var scale = this.config.maxWidth / canvas.width; scaledCanvas.width = canvas.width * scale; scaledCanvas.height = canvas.height * scale; var srcImgData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height); var destImgData = scaledCanvas.getContext('2d').createImageData(scaledCanvas.width, scaledCanvas.height); this.applyBilinearInterpolation(srcImgData, destImgData, scale); scaledCanvas.getContext('2d').putImageData(destImgData, 0, 0); return scaledCanvas; }; ImageUploader.prototype.getHalfScaleCanvas = function(canvas) { var halfCanvas = document.createElement('canvas'); halfCanvas.width = canvas.width / 2; halfCanvas.height = canvas.height / 2; halfCanvas.getContext('2d').drawImage(canvas, 0, 0, halfCanvas.width, halfCanvas.height); return halfCanvas; }; ImageUploader.prototype.applyBilinearInterpolation = function(srcCanvasData, destCanvasData, scale) { function inner(f00, f10, f01, f11, x, y) { var un_x = 1.0 - x; var un_y = 1.0 - y; return (f00 * un_x * un_y + f10 * x * un_y + f01 * un_x * y + f11 * x * y); } var i, j; var iyv, iy0, iy1, ixv, ix0, ix1; var idxD, idxS00, idxS10, idxS01, idxS11; var dx, dy; var r, g, b, a; for (i = 0; i < destCanvasData.height; ++i) { iyv = i / scale; iy0 = Math.floor(iyv); // Math.ceil can go over bounds iy1 = (Math.ceil(iyv) > (srcCanvasData.height - 1) ? (srcCanvasData.height - 1) : Math.ceil(iyv)); for (j = 0; j < destCanvasData.width; ++j) { ixv = j / scale; ix0 = Math.floor(ixv); // Math.ceil can go over bounds ix1 = (Math.ceil(ixv) > (srcCanvasData.width - 1) ? (srcCanvasData.width - 1) : Math.ceil(ixv)); idxD = (j + destCanvasData.width * i) * 4; // matrix to vector indices idxS00 = (ix0 + srcCanvasData.width * iy0) * 4; idxS10 = (ix1 + srcCanvasData.width * iy0) * 4; idxS01 = (ix0 + srcCanvasData.width * iy1) * 4; idxS11 = (ix1 + srcCanvasData.width * iy1) * 4; // overall coordinates to unit square dx = ixv - ix0; dy = iyv - iy0; // I let the r, g, b, a on purpose for debugging r = inner(srcCanvasData.data[idxS00], srcCanvasData.data[idxS10], srcCanvasData.data[idxS01], srcCanvasData.data[idxS11], dx, dy); destCanvasData.data[idxD] = r; g = inner(srcCanvasData.data[idxS00 + 1], srcCanvasData.data[idxS10 + 1], srcCanvasData.data[idxS01 + 1], srcCanvasData.data[idxS11 + 1], dx, dy); destCanvasData.data[idxD + 1] = g; b = inner(srcCanvasData.data[idxS00 + 2], srcCanvasData.data[idxS10 + 2], srcCanvasData.data[idxS01 + 2], srcCanvasData.data[idxS11 + 2], dx, dy); destCanvasData.data[idxD + 2] = b; a = inner(srcCanvasData.data[idxS00 + 3], srcCanvasData.data[idxS10 + 3], srcCanvasData.data[idxS01 + 3], srcCanvasData.data[idxS11 + 3], dx, dy); destCanvasData.data[idxD + 3] = a; } } }; 

Esto escala una imagen hasta un ancho de config.maxWidth , manteniendo la relación de aspecto original. En el momento del desarrollo, esto funcionó en iPad / iPhone Safari además de los principales navegadores de escritorio (IE9 +, Firefox, Chrome), así que espero que siga siendo compatible debido a la mayor aceptación de HTML5 en la actualidad. Tenga en cuenta que la llamada canvas.toDataURL () toma un tipo de mimo y calidad de imagen que le permitirá controlar la calidad y el formato del archivo de salida (potencialmente diferente a la entrada si lo desea).

El único punto que esto no cubre es mantener la información de orientación, sin conocimiento de estos metadatos, la imagen cambia de tamaño y se guarda como está, perdiendo cualquier metadato dentro de la imagen para orientación, lo que significa que las imágenes tomadas en un dispositivo de tableta “boca abajo” representados como tales, aunque habrían sido volteados en el visor de la cámara del dispositivo. Si esto es una preocupación, esta publicación de blog tiene una buena guía y ejemplos de código sobre cómo lograr esto, que estoy seguro podría integrarse al código anterior.

Corrección a lo anterior:

    

Modificación a la respuesta de Justin que funciona para mí:

  1. Añadido img.onload
  2. Expande la solicitud POST con un ejemplo real
 function handleFiles() { var dataurl = null; var filesToUpload = document.getElementById('photo').files; var file = filesToUpload[0]; // Create an image var img = document.createElement("img"); // Create a file reader var reader = new FileReader(); // Set the image once loaded into file reader reader.onload = function(e) { img.src = e.target.result; img.onload = function () { var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); var MAX_WIDTH = 800; var MAX_HEIGHT = 600; var width = img.width; var height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); dataurl = canvas.toDataURL("image/jpeg"); // Post the data var fd = new FormData(); fd.append("name", "some_filename.jpg"); fd.append("image", dataurl); fd.append("info", "lah_de_dah"); $.ajax({ url: '/ajax_photo', data: fd, cache: false, contentType: false, processData: false, type: 'POST', success: function(data){ $('#form_photo')[0].reset(); location.reload(); } }); } // img.onload } // Load files into file reader reader.readAsDataURL(file); } 

Si no quieres reinventar la rueda, puedes probar plupload.com

 fd.append("image", dataurl); 

Esto no funcionará. En PHP, no puede guardar el archivo con esto.

Use este código en su lugar:

 var blobBin = atob(dataurl.split(',')[1]); var array = []; for(var i = 0; i < blobBin.length; i++) { array.push(blobBin.charCodeAt(i)); } var file = new Blob([new Uint8Array(array)], {type: 'image/png', name: "avatar.png"}); fd.append("image", file); // blob file 

La respuesta aceptada funciona de maravilla, pero la lógica de cambio de tamaño ignora el caso en el que la imagen es más grande que el máximo en solo uno de los ejes (por ejemplo, alto> máximoAlto pero ancho < = ancho máximo).

Creo que el siguiente código se ocupa de todos los casos de una manera más directa y funcional (ignore las anotaciones de tipo typescript si usa javascript simple):

 private scaleDownSize(width: number, height: number, maxWidth: number, maxHeight: number): {width: number, height: number} { if (width < = maxWidth && height <= maxHeight) return { width, height }; else if (width / maxWidth > height / maxHeight) return { width: maxWidth, height: height * maxWidth / width}; else return { width: width * maxHeight / height, height: maxHeight }; } 

Cambiar el tamaño de las imágenes en un elemento de canvas suele ser una mala idea, ya que utiliza la interpolación de caja más barata. La imagen resultante se degrada en calidad. Recomiendo usar http://nodeca.github.io/pica/demo/ que puede realizar la transformación Lanczos en su lugar. La página de demostración anterior muestra la diferencia entre el canvas y los enfoques de Lanczos.

También utiliza trabajadores web para cambiar el tamaño de las imágenes en paralelo. También hay implementación de WEBGL.

Hay algunos modificadores de imagen en línea que usan pica para hacer el trabajo, como https://myimageresizer.com

Puede usar dropzone.js si quiere usar un administrador de carga simple y fácil con cambio de tamaño antes de cargar funciones.

Tiene funciones de cambio de tamaño integradas, pero puede proporcionarlas si lo desea.

Mecanografiado

 async resizeImg(file: Blob): Promise { let img = document.createElement("img"); img.src = await new Promise(resolve => { let reader = new FileReader(); reader.onload = (e: any) => resolve(e.target.result); reader.readAsDataURL(file); }); await new Promise(resolve => img.onload = resolve) let canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let MAX_WIDTH = 1000; let MAX_HEIGHT = 1000; let width = img.naturalWidth; let height = img.naturalHeight; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); let result = await new Promise(resolve => { canvas.toBlob(resolve, 'image/jpeg', 0.95); }); return result; }