Comment compresser une image via Javascript dans le navigateur?


92

TL, DR;

Existe-t-il un moyen de compresser une image (principalement jpeg, png et gif) directement côté navigateur, avant de la télécharger? Je suis presque sûr que JavaScript peut le faire, mais je ne trouve pas de moyen d'y parvenir.


Voici le scénario complet que je voudrais mettre en œuvre:

  • l'utilisateur accède à mon site web, et choisit une image via un input type="file"élément,
  • cette image est récupérée via JavaScript, nous effectuons des vérifications telles que le format de fichier correct, la taille maximale du fichier, etc.
  • si tout va bien, un aperçu de l'image s'affiche sur la page,
  • l'utilisateur peut effectuer certaines opérations de base comme faire pivoter l'image de 90 ° / -90 °, la recadrer suivant un rapport prédéfini, etc., ou l'utilisateur peut télécharger une autre image et revenir à l'étape 1
  • lorsque l'utilisateur est satisfait, l'image éditée est alors compressée et "enregistrée" localement (non enregistrée dans un fichier, mais dans la mémoire / page du navigateur), -
  • l'utilisateur remplit un formulaire avec des données telles que son nom, son âge, etc.
  • l'utilisateur clique sur le bouton "Terminer", puis le formulaire contenant les données + image compressée est envoyé au serveur (sans AJAX),

Le processus complet jusqu'à la dernière étape doit être effectué côté client et doit être compatible avec les derniers Chrome et Firefox, Safari 5+ et IE 8+ . Si possible, seul JavaScript doit être utilisé (mais je suis presque sûr que ce n'est pas possible).

Je n'ai rien codé pour le moment, mais j'y ai déjà pensé. La lecture de fichiers localement est possible via File API , la prévisualisation et l'édition d'image peuvent être effectuées à l'aide de l' élément Canvas , mais je ne trouve pas de moyen de faire la partie compression d'image .

Selon html5please.com et caniuse.com , la prise en charge de ces navigateurs est assez difficile (grâce à IE), mais pourrait être réalisée à l'aide de polyfill tels que FlashCanvas et FileReader .

En fait, l'objectif est de réduire la taille du fichier, donc je vois la compression d'image comme une solution. Mais, je sais que les images téléchargées vont être affichées sur mon site Web, à chaque fois au même endroit, et je connais la dimension de cette zone d'affichage (par exemple. 200x400). Ainsi, je pourrais redimensionner l'image pour l'adapter à ces dimensions, réduisant ainsi la taille du fichier. Je n'ai aucune idée du taux de compression de cette technique.

Qu'est-ce que tu penses ? Avez-vous des conseils à me dire? Connaissez-vous un moyen de compresser une image côté navigateur en JavaScript? Merci pour vos réponses.

Réponses:


165

En bref:

  • Lisez les fichiers à l'aide de l'API HTML5 FileReader avec .readAsArrayBuffer
  • Créez un blob avec les données du fichier et obtenez son URL avec window.URL.createObjectURL (blob)
  • Créez un nouvel élément Image et définissez son src sur l'URL du fichier blob
  • Envoyez l'image sur le canevas. La taille de la toile est définie sur la taille de sortie souhaitée
  • Récupérez les données à échelle réduite de canvas via canvas.toDataURL ("image / jpeg", 0.7) (définissez votre propre format et qualité de sortie)
  • Attachez de nouvelles entrées cachées au formulaire d'origine et transférez les images dataURI essentiellement sous forme de texte normal
  • Sur le backend, lisez le dataURI, décodez à partir de Base64 et enregistrez-le

Source: code .


1
Merci beaucoup ! C'est ce que je cherchais. Savez-vous à quel point le taux de compression est bon avec cette technique?
pomeh le

2
@NicholasKyriakides Je peux confirmer que canvas.toDataURL("image/jpeg",0.7)le compresse efficacement, il enregistre JPEG avec une qualité 70 (par opposition à la qualité par défaut, 100).
user1111929

4
@Nicholas Kyriakides, ce n'est pas une bonne distinction à faire. La plupart des codecs ne sont pas sans perte, ils s'intègrent donc dans votre définition de «réduction d'échelle» (c'est-à-dire que vous ne pouvez pas revenir à 100).
Billybobbonnet

5
La réduction d'échelle consiste à créer des images de plus petite taille en termes de hauteur et de largeur. C'est vraiment de la compression. C'est une compression avec perte mais certainement une compression. Il ne réduit pas la taille des pixels, il ne fait que pousser certains des pixels à avoir la même couleur afin que la compression puisse atteindre ces couleurs en moins de bits. JPEG a de toute façon intégré la compression pour les pixels, mais en mode avec perte, il est dit que quelques couleurs désactivées peuvent être appelées la même couleur. C'est toujours la compression. La mise à l'échelle des graphiques fait généralement référence à une modification de la taille réelle.
Tatarize le

3
Je veux juste dire ceci: le fichier peut aller directement URL.createObjectUrl()sans le transformer en un blob; le fichier compte comme un objet blob.
hellol11

20

Je vois deux choses qui manquent dans les autres réponses:

  • canvas.toBlob(si disponible) est plus performant que canvas.toDataURL, et aussi asynchrone.
  • le fichier -> image -> canevas -> la conversion de fichier perd les données EXIF; en particulier, les données sur la rotation des images communément définies par les téléphones / tablettes modernes.

Le script suivant traite des deux points:

// 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);
}

Exemple d'utilisation:

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 a fait l'affaire pour moi, en créant un fichier et en recevant le tableau $ _FILES sur le serveur. Merci!
Ricardo Ruiz Romero

Ça a l'air génial! Mais cela fonctionnera-t-il sur tous les navigateurs, Web et mobiles? (
Ignorons

14

La réponse de @PsychoWoods est bonne. Je souhaite proposer ma propre solution. Cette fonction Javascript prend une URL de données d'image et une largeur, la met à l'échelle à la nouvelle largeur et renvoie une nouvelle URL de données.

// 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;
}

Ce code peut être utilisé partout où vous avez une URL de données et souhaitez une URL de données pour une image réduite.


s'il vous plaît, pouvez-vous me donner plus de détails sur cet exemple, comment appeler la fonction et comment a renvoyé le résultat?
visulo

Voici un exemple: danielsadventure.info/Html/scaleimage.html Assurez-vous de lire la source de la page pour voir comment cela fonctionne.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… Pour les autres personnes souhaitant lire le lien proposé par @DanielAllenLangdon
Cedric Arnould

En guise d'avertissement, parfois image.width / height retournera 0 car il n'a pas été chargé. Vous devrez peut-être convertir cela en une fonction asynchrone et écouter image.onload pour obtenir l'image correcte avec et hauteur.
Matt Pope


4

J'ai eu un problème avec la downscaleImage()fonction publiée ci-dessus par @ daniel-allen-langdon en ce que les propriétés image.widthet image.heightne sont pas disponibles immédiatement car le chargement de l'image est asynchrone .

Veuillez consulter l'exemple TypeScript mis à jour ci-dessous qui prend cela en compte, utilise des asyncfonctions et redimensionne l'image en fonction de la dimension la plus longue plutôt que de la largeur

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;
    }

}

J'ajouterais l'explication de quality, resolutionet imageType(format de ceci)
Barabas

3

Edit: Selon le commentaire de M. Me sur cette réponse, il semble que la compression soit maintenant disponible pour les formats JPG / WebP (voir https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

Pour autant que je sache, vous ne pouvez pas compresser les images à l'aide de la toile, mais vous pouvez la redimensionner. L'utilisation de canvas.toDataURL ne vous permettra pas de choisir le taux de compression à utiliser. Vous pouvez jeter un œil à canimage qui fait exactement ce que vous voulez: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

En fait, il suffit souvent de redimensionner l'image pour diminuer sa taille, mais si vous voulez aller plus loin, vous devrez utiliser la méthode nouvellement introduite file.readAsArrayBuffer pour obtenir un tampon contenant les données d'image.

Ensuite, utilisez simplement un DataView pour lire son contenu conformément à la spécification du format d'image ( http://en.wikipedia.org/wiki/JPEG ou http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Il sera difficile de gérer la compression des données d'image, mais c'est pire à essayer. D'un autre côté, vous pouvez essayer de supprimer les en-têtes PNG ou les données JPEG exif pour rendre votre image plus petite, cela devrait être plus facile à faire.

Vous devrez créer un autre DataWiew sur un autre tampon et le remplir avec le contenu de l'image filtrée. Ensuite, il vous suffira d'encoder le contenu de votre image dans DataURI à l'aide de window.btoa.

Faites-moi savoir si vous implémentez quelque chose de similaire, il sera intéressant de parcourir le code.


Peut-être que quelque chose a changé depuis que vous avez publié ceci, mais le deuxième argument de cette fonction canvas.toDataURL que vous avez mentionnée est la quantité de compression que vous souhaitez appliquer.
wp-overwatch.com

2

Je trouve qu'il existe une solution plus simple par rapport à la réponse acceptée.

  • Lisez les fichiers à l'aide de l'API HTML5 FileReader avec .readAsArrayBuffer
  • Créez un objet blob avec les données du fichier et obtenez son URL avec window.URL.createObjectURL(blob)
  • Créez un nouvel élément Image et définissez son src sur l'URL du fichier blob
  • Envoyez l'image sur le canevas. La taille de la toile est définie sur la taille de sortie souhaitée
  • Récupérez les données à l'échelle réduite de la toile via canvas.toDataURL("image/jpeg",0.7)(définissez votre propre format et qualité de sortie)
  • Attachez de nouvelles entrées cachées au formulaire d'origine et transférez les images dataURI essentiellement sous forme de texte normal
  • Sur le backend, lisez le dataURI, décodez à partir de Base64 et enregistrez-le

Selon votre question:

Existe-t-il un moyen de compresser une image (principalement jpeg, png et gif) directement côté navigateur, avant de la télécharger

Ma solution:

  1. Créez un objet blob avec le fichier directement avec URL.createObjectURL(inputFileElement.files[0]).

  2. Identique à la réponse acceptée.

  3. Identique à la réponse acceptée. Il convient de mentionner que la taille de la toile est nécessaire et à utiliser img.widthet img.heightà définir canvas.widthet canvas.height. Non img.clientWidth.

  4. Obtenez l'image de réduction par canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Le réglage 'image/jpg'n'a aucun effet. image/pngest également pris en charge. Créez un nouvel Fileobjet à l'intérieur du callbackfunction corps avec let compressedImageBlob = new File([blob]).

  5. Ajoutez de nouvelles entrées cachées ou envoyez via javascript . Le serveur n'a rien à décoder.

Consultez https://javascript.info/binary pour toutes les informations. J'ai trouvé la solution après avoir lu ce chapitre.


Code:

    <!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>

Ce code semble beaucoup moins effrayant que les autres réponses.

Mise à jour:

Il faut tout mettre à l'intérieur img.onload. Sinon canvas, vous ne pourrez pas obtenir correctement la largeur et la hauteur de l'image lorsque l'heure canvasest attribuée.

    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 .pngtest de compression de fichier avec image/jpegjeu d'arguments.

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


0

J'ai amélioré la fonction d'une tête pour être ceci:

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);
    });
  };

son utilisation:

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

prendre plaisir ;)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.