¿Cómo comprimir una imagen a través de Javascript en el navegador?


92

TL; DR;

¿Hay alguna forma de comprimir una imagen (principalmente jpeg, png y gif) directamente en el navegador antes de cargarla? Estoy bastante seguro de que JavaScript puede hacer esto, pero no puedo encontrar la manera de lograrlo.


Aquí está el escenario completo que me gustaría implementar:

  • el usuario va a mi sitio web y elige una imagen a través de un input type="file"elemento,
  • esta imagen se recupera a través de JavaScript, realizamos algunas verificaciones, como el formato de archivo correcto, el tamaño máximo de archivo, etc.
  • si todo está bien, se muestra una vista previa de la imagen en la página,
  • el usuario puede realizar algunas operaciones básicas como rotar la imagen 90 ° / -90 °, recortarla siguiendo una proporción predefinida, etc., o el usuario puede cargar otra imagen y volver al paso 1,
  • cuando el usuario está satisfecho, la imagen editada se comprime y "guarda" localmente (no se guarda en un archivo, sino en la memoria / página del navegador), -
  • el usuario llena un formulario con datos como nombre, edad, etc.
  • el usuario hace clic en el botón "Finalizar", luego el formulario que contiene los datos + imagen comprimida se envía al servidor (sin AJAX),

El proceso completo hasta el último paso debe realizarse del lado del cliente y debe ser compatible con los últimos Chrome y Firefox, Safari 5+ e IE 8+ . Si es posible, solo debe usarse JavaScript (pero estoy bastante seguro de que esto no es posible).

No he codificado nada en este momento, pero ya lo he pensado. La lectura de archivos localmente es posible a través de la API de archivos , la vista previa y la edición de imágenes se pueden realizar utilizando el elemento Canvas , pero no puedo encontrar una manera de hacer la parte de compresión de imágenes .

Según html5please.com y caniuse.com , admitir esos navegadores es bastante difícil (gracias a IE), pero se podría hacer usando polyfill como FlashCanvas y FileReader .

En realidad, el objetivo es reducir el tamaño del archivo, por lo que veo la compresión de imágenes como una solución. Pero sé que las imágenes cargadas se mostrarán en mi sitio web, siempre en el mismo lugar, y conozco la dimensión de esta área de visualización (por ejemplo, 200x400). Entonces, podría cambiar el tamaño de la imagen para que se ajuste a esas dimensiones, reduciendo así el tamaño del archivo. No tengo idea de cuál sería la relación de compresión para esta técnica.

Qué piensas ? ¿Tienes algún consejo que darme? ¿Conoce alguna forma de comprimir una imagen del lado del navegador en JavaScript? Gracias por tus respuestas.

Respuestas:


165

En breve:

  • Lea los archivos utilizando la API FileReader HTML5 con .readAsArrayBuffer
  • Cree un Blob con los datos del archivo y obtenga su URL con window.URL.createObjectURL (blob)
  • Cree un nuevo elemento de imagen y establezca su src en la URL del blob del archivo
  • Envía la imagen al lienzo. El tamaño del lienzo se establece en el tamaño de salida deseado
  • Obtenga los datos reducidos del lienzo a través de canvas.toDataURL ("image / jpeg", 0.7) (establezca su propio formato y calidad de salida)
  • Adjunte nuevas entradas ocultas al formulario original y transfiera las imágenes dataURI básicamente como texto normal
  • En el backend, lea el dataURI, decodifique desde Base64 y guárdelo

Fuente: código .


1
Muchas gracias ! Esto es lo que estaba buscando. ¿Sabes qué tan buena es la relación de compresión con esta técnica?
pomeh

2
@NicholasKyriakides Puedo confirmar que canvas.toDataURL("image/jpeg",0.7)efectivamente lo comprime, guarda JPEG con calidad 70 (en contraposición al valor predeterminado, calidad 100).
user1111929

4
@Nicholas Kyriakides, esta no es una buena distinción. La mayoría de los códecs no están exentos de pérdidas, por lo que encajarían en su definición de "reducción de escala" (es decir, no puede volver a 100).
Billybobbonnet

5
La reducción de escala se refiere a hacer imágenes de un tamaño más pequeño en términos de altura y ancho. Esto realmente es compresión. Es compresión con pérdida pero ciertamente compresión. No está reduciendo la escala de los píxeles, solo empuja algunos de los píxeles para que sean del mismo color para que la compresión pueda alcanzar esos colores en menos bits. JPEG ha incorporado compresión para los píxeles de todos modos, pero en el modo con pérdida dice que algunos colores apagados pueden llamarse del mismo color. Eso sigue siendo compresión. La reducción de escala con respecto a los gráficos generalmente se refiere a un cambio en el tamaño real.
Tatarize

3
Solo quiero decir esto: el archivo puede ir directamente URL.createObjectUrl()sin convertir el archivo en un blob; el archivo cuenta como un blob.
hellol11

20

Veo dos cosas que faltan en las otras respuestas:

  • canvas.toBlob(cuando esté disponible) es más eficaz que canvas.toDataURL, y también asincrónico.
  • el archivo -> imagen -> lienzo -> conversión de archivo pierde datos EXIF; en particular, los datos sobre la rotación de imágenes establecidos comúnmente por teléfonos / tabletas modernos.

El siguiente guión trata de ambos puntos:

// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
    Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
        value: function(callback, type, quality) {

            var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
                len = binStr.length,
                arr = new Uint8Array(len);

            for (var i = 0; i < len; i++) {
                arr[i] = binStr.charCodeAt(i);
            }

            callback(new Blob([arr], {type: type || 'image/png'}));
        }
    });
}

window.URL = window.URL || window.webkitURL;

// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
    // Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
    if (file.slice) {
        file = file.slice(0, 131072);
    } else if (file.webkitSlice) {
        file = file.webkitSlice(0, 131072);
    }

    var reader = new FileReader();
    reader.onload = function(e) {
        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8) {
            callback(-2);
            return;
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) {
                if (view.getUint32(offset += 2, false) != 0x45786966) {
                    callback(-1);
                    return;
                }
                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;
                for (var i = 0; i < tags; i++)
                    if (view.getUint16(offset + (i * 12), little) == 0x0112) {
                        callback(view.getUint16(offset + (i * 12) + 8, little));
                        return;
                    }
            }
            else if ((marker & 0xFF00) != 0xFF00) break;
            else offset += view.getUint16(offset, false);
        }
        callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
    var canvas = document.createElement('canvas');
    if (orientation > 4) {
        canvas.width = rawHeight;
        canvas.height = rawWidth;
    } else {
        canvas.width = rawWidth;
        canvas.height = rawHeight;
    }

    if (orientation > 1) {
        console.log("EXIF orientation = " + orientation + ", rotating picture");
    }

    var ctx = canvas.getContext('2d');
    switch (orientation) {
        case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
        case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
        case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
        case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
        case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
        case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
    }
    ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
    return canvas;
}

function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
    if (file.size <= acceptFileSize) {
        callback(file);
        return;
    }
    var img = new Image();
    img.onerror = function() {
        URL.revokeObjectURL(this.src);
        callback(file);
    };
    img.onload = function() {
        URL.revokeObjectURL(this.src);
        getExifOrientation(file, function(orientation) {
            var w = img.width, h = img.height;
            var scale = (orientation > 4 ?
                Math.min(maxHeight / w, maxWidth / h, 1) :
                Math.min(maxWidth / w, maxHeight / h, 1));
            h = Math.round(h * scale);
            w = Math.round(w * scale);

            var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
            canvas.toBlob(function(blob) {
                console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
                callback(blob);
            }, 'image/jpeg', quality);
        });
    };
    img.src = URL.createObjectURL(file);
}

Uso de ejemplo:

inputfile.onchange = function() {
    // If file size > 500kB, resize such that width <= 1000, quality = 0.9
    reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
        let body = new FormData();
        body.set('file', blob, blob.name || "file.jpg");
        fetch('/upload-image', {method: 'POST', body}).then(...);
    });
};

ToBlob hizo el truco por mí, creando un archivo y recibiendo en la matriz $ _FILES en el servidor. ¡Gracias!
Ricardo Ruiz Romero

¡Se ve muy bien! Pero, ¿funcionará esto en todos los navegadores, web y móviles? (
Ignoremos

14

La respuesta de @PsychoWoods es buena. Me gustaría ofrecer mi propia solución. Esta función de Javascript toma una URL de datos de imagen y un ancho, la escala al nuevo ancho y devuelve una nueva URL de datos.

// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
    "use strict";
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;

    // Provide default values
    imageType = imageType || "image/jpeg";
    imageArguments = imageArguments || 0.7;

    // Create a temporary image so that we can compute the height of the downscaled image.
    image = new Image();
    image.src = dataUrl;
    oldWidth = image.width;
    oldHeight = image.height;
    newHeight = Math.floor(oldHeight / oldWidth * newWidth)

    // Create a temporary canvas to draw the downscaled image on.
    canvas = document.createElement("canvas");
    canvas.width = newWidth;
    canvas.height = newHeight;

    // Draw the downscaled image on the canvas and return the new data URL.
    ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, newWidth, newHeight);
    newDataUrl = canvas.toDataURL(imageType, imageArguments);
    return newDataUrl;
}

Este código se puede usar en cualquier lugar donde tenga una URL de datos y desee una URL de datos para una imagen reducida.


por favor, ¿puede darme más detalles sobre este ejemplo, cómo llamar a la función y cómo devolvió el resultado?
visulo

Aquí hay un ejemplo: danielsadventure.info/Html/scaleimage.html Asegúrese de leer la fuente de la página para ver cómo funciona.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… Para otras personas que quieran leer el enlace propuesto por @DanielAllenLangdon
Cedric Arnould

Como aviso, a veces image.width / height devolverá 0 ya que no se ha cargado. Es posible que deba convertir esto en una función asíncrona y escuchar image.onload para obtener la imagen correcta con una altura.
Matt Pope


4

Tuve un problema con la downscaleImage()función publicada anteriormente por @ daniel-allen-langdon porque las propiedades image.widthy image.heightno están disponibles de inmediato porque la carga de la imagen es asincrónica .

Consulte el ejemplo actualizado de TypeScript a continuación que tiene esto en cuenta, utiliza asyncfunciones y cambia el tamaño de la imagen según la dimensión más larga en lugar de solo el ancho.

function getImage(dataUrl: string): Promise<HTMLImageElement> 
{
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.src = dataUrl;
        image.onload = () => {
            resolve(image);
        };
        image.onerror = (el: any, err: ErrorEvent) => {
            reject(err.error);
        };
    });
}

export async function downscaleImage(
        dataUrl: string,  
        imageType: string,  // e.g. 'image/jpeg'
        resolution: number,  // max width/height in pixels
        quality: number   // e.g. 0.9 = 90% quality
    ): Promise<string> {

    // Create a temporary image so that we can compute the height of the image.
    const image = await getImage(dataUrl);
    const oldWidth = image.naturalWidth;
    const oldHeight = image.naturalHeight;
    console.log('dims', oldWidth, oldHeight);

    const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
    const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
    console.log('longest dim', longestDimension, currentRes);

    if (currentRes > resolution) {
        console.log('need to resize...');

        // Calculate new dimensions
        const newSize = longestDimension == 'width'
            ? Math.floor(oldHeight / oldWidth * resolution)
            : Math.floor(oldWidth / oldHeight * resolution);
        const newWidth = longestDimension == 'width' ? resolution : newSize;
        const newHeight = longestDimension == 'height' ? resolution : newSize;
        console.log('new width / height', newWidth, newHeight);

        // Create a temporary canvas to draw the downscaled image on.
        const canvas = document.createElement('canvas');
        canvas.width = newWidth;
        canvas.height = newHeight;

        // Draw the downscaled image on the canvas and return the new data URL.
        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(image, 0, 0, newWidth, newHeight);
        const newDataUrl = canvas.toDataURL(imageType, quality);
        return newDataUrl;
    }
    else {
        return dataUrl;
    }

}

Yo añadiría la explicación de quality, resolutiony imageType(formato de este)
Barabas

3

Editar: según el comentario de Mr Me sobre esta respuesta, parece que la compresión ahora está disponible para formatos JPG / WebP (consulte https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

Hasta donde yo sé, no puede comprimir imágenes usando lienzo, en su lugar, puede cambiar su tamaño. El uso de canvas.toDataURL no le permitirá elegir la relación de compresión a utilizar. Puedes echar un vistazo a canimage que hace exactamente lo que quieres: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

De hecho, a menudo es suficiente cambiar el tamaño de la imagen para disminuir su tamaño, pero si desea ir más allá, tendrá que usar el método file.readAsArrayBuffer recién introducido para obtener un búfer que contenga los datos de la imagen.

Luego, simplemente use un DataView para leer su contenido de acuerdo con la especificación del formato de imagen ( http://en.wikipedia.org/wiki/JPEG o http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Será difícil lidiar con la compresión de datos de imágenes, pero es peor intentarlo. Por otro lado, puede intentar eliminar los encabezados PNG o los datos exif JPEG para hacer su imagen más pequeña, debería ser más fácil hacerlo.

Tendrá que crear otro DataWiew en otro búfer y llenarlo con el contenido de la imagen filtrada. Luego, solo tendrá que codificar el contenido de su imagen en DataURI usando window.btoa.

Avíseme si implementa algo similar, será interesante revisar el código.


Quizás algo haya cambiado desde que publicó esto, pero el segundo argumento de la función canvas.toDataURL que mencionó es la cantidad de compresión que desea aplicar.
wp-overwatch.com

2

Encuentro que hay una solución más simple en comparación con la respuesta aceptada.

  • Lea los archivos utilizando la API FileReader HTML5 con .readAsArrayBuffer
  • Cree un Blob con los datos del archivo y obtenga su URL con window.URL.createObjectURL(blob)
  • Cree un nuevo elemento de imagen y establezca su src en la URL del blob del archivo
  • Envía la imagen al lienzo. El tamaño del lienzo se establece en el tamaño de salida deseado
  • Obtenga los datos reducidos del lienzo a través de canvas.toDataURL("image/jpeg",0.7)(configure su propio formato y calidad de salida)
  • Adjunte nuevas entradas ocultas al formulario original y transfiera las imágenes dataURI básicamente como texto normal
  • En el backend, lea el dataURI, decodifique desde Base64 y guárdelo

Según tu pregunta:

¿Hay alguna forma de comprimir una imagen (principalmente jpeg, png y gif) directamente en el navegador, antes de cargarla?

Mi solución:

  1. Cree un blob con el archivo directamente con URL.createObjectURL(inputFileElement.files[0]).

  2. Igual que la respuesta aceptada.

  3. Igual que la respuesta aceptada. Vale la pena mencionar que el tamaño del lienzo es necesario y usar img.widthy img.heightconfigurar canvas.widthy canvas.height. No img.clientWidth.

  4. Obtenga la imagen reducida por canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). El ajuste 'image/jpg'no tiene ningún efecto. image/pngtambién es compatible. Haz un nuevo Fileobjeto dentro del callbackfunction cuerpo con let compressedImageBlob = new File([blob]).

  5. Agregue nuevas entradas ocultas o envíe a través de javascript . El servidor no tiene que decodificar nada.

Consulte https://javascript.info/binary para obtener toda la información. Se me ocurrió la solución después de leer este capítulo.


Código:

    <!DOCTYPE html>
    <html>
    <body>
    <form action="upload.php" method="post" enctype="multipart/form-data">
      Select image to upload:
      <input type="file" name="fileToUpload" id="fileToUpload" multiple>
      <input type="submit" value="Upload Image" name="submit">
    </form>
    </body>
    </html>

Este código parece mucho menos aterrador que las otras respuestas.

Actualizar:

Hay que meterlo todo dentro img.onload. De canvaslo contrario , no podrá obtener el ancho y el alto de la imagen correctamente a medida que canvasse asigna el tiempo .

    function upload(){
        var f = fileToUpload.files[0];
        var fileName = f.name.split('.')[0];
        var img = new Image();
        img.src = URL.createObjectURL(f);
        img.onload = function(){
            var canvas = document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            canvas.toBlob(function(blob){
                    console.info(blob.size);
                    var f2 = new File([blob], fileName + ".jpeg");
                    var xhr = new XMLHttpRequest();
                    var form = new FormData();
                    form.append("fileToUpload", f2);
                    xhr.open("POST", "upload.php");
                    xhr.send(form);
            }, 'image/jpeg', 0.5);
        }
    }

3.4MB .pngprueba de compresión de archivos con image/jpegconjunto de argumentos.

    |0.9| 777KB |
    |0.8| 383KB |
    |0.7| 301KB |
    |0.6| 251KB |
    |0.5| 219kB |


0

Mejoré la función a head para que sea esto:

var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
    (new Promise(function(resolve){
      image = new Image(); image.src = dataUrl;
      log(image);
      resolve('Done : ');
    })).then((d)=>{
      oldWidth = image.width; oldHeight = image.height;
      log([oldWidth,oldHeight]);
      newHeight = Math.floor(oldHeight / oldWidth * newWidth);
      log(d+' '+newHeight);

      canvas = document.createElement("canvas");
      canvas.width = newWidth; canvas.height = newHeight;
      log(canvas);
      ctx = canvas.getContext("2d");
      ctx.drawImage(image, 0, 0, newWidth, newHeight);
      //log(ctx);
      newDataUrl = canvas.toDataURL(imageType, imageArguments);
      resolve(newDataUrl);
    });
  };

el uso de la misma:

minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
   console.log(data); // the new DATAURL
});

disfrutar;)

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.