web-dev-qa-db-fra.com

Comment compresser une image via Javascript dans le navigateur?

TL; DR;

Existe-t-il un moyen de compresser une image (principalement au format jpeg, png et gif) directement sur le navigateur, avant de la télécharger? Je suis sûr que JavaScript peut le faire, mais je ne trouve pas le moyen de le réaliser.


Voici le scénario complet que je voudrais implémenter:

  • l'utilisateur accède à mon site Web et choisit une image via un élément input type="file",
  • cette image est récupérée via JavaScript, nous effectuons des vérifications telles que le format de fichier correct, la taille de fichier maximale, etc.,
  • si tout va bien, un aperçu de l'image est affiché sur la page,
  • l'utilisateur peut effectuer certaines opérations de base, telles que faire pivoter l'image de 90 °/90 °, la recadrer selon 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 modifiée est ensuite compressée et "enregistrée" localement (non enregistrée dans un fichier, mais dans la mémoire/la page du navigateur), -
  • l'utilisateur remplit un formulaire avec des données telles que le nom, l'âge, etc.
  • l'utilisateur clique sur le bouton "Terminer", le formulaire contenant les données + l'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 versions les plus récentes de Chrome et Firefox, Safari 5+ et IE 8+. Si possible, seul JavaScript devrait être utilisé (mais je suis à peu près 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 fichier en local est possible via API de fichier , la prévisualisation et l'édition d'images peuvent être effectuées à l'aide de l'élément Canvas , mais je ne trouve pas le moyen de procéder à la compression d'image .

Selon (html5please.com et caniuse.com , supporter ces navigateurs est assez difficile (grâce à IE), mais pourrait être fait avec polyfill comme FlashCanvas et FileReader .

En réalité, l’objectif est de réduire la taille du fichier. La compression d’image est donc 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). Je pouvais donc redimensionner l'image pour l'adapter à ces dimensions, réduisant ainsi la taille du fichier. Je n'ai aucune idée de ce que serait le taux de compression pour 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.

58
pomeh

En bref:

  • Lire 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 sa src sur le fichier blob url 
  • Envoyez l'image sur la toile. La taille de la zone de travail est définie sur la taille de sortie souhaitée
  • Récupérez les données réduites de la toile via canvas.toDataURL ("image/jpeg", 0.7) (définissez votre propre format et qualité de sortie)
  • Attachez de nouvelles entrées masquées au formulaire d'origine et transférez les images dataURI en tant que texte normal
  • Sur le backend, lisez le dataURI, décodez à partir de Base64 et enregistrez-le

Source: code .

108
psychowood

La réponse de @PsychoWoods est bonne. Je voudrais offrir ma propre solution. Cette fonction Javascript prend une URL de données d'image et une largeur, la redimensionne à 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.

11

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

  • canvas.toBlob (si disponible) est plus performant que canvas.toDataURL et asynchrone.
  • le fichier -> image -> toile -> la conversion de fichier perd les données EXIF; en particulier, les données sur la rotation des images généralement 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(...);
    });
};
9
Simon Lindholm

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 ).

Autant que je sache, vous ne pouvez pas compresser les images à l'aide de la toile, mais vous pouvez le redimensionner. Utiliser canvas.toDataURL ne vous laissera pas choisir le taux de compression à utiliser. Vous pouvez consulter 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 simplement redimensionner l’image pour la réduire, mais si vous voulez aller plus loin, vous devrez utiliser la méthode récemment introduite file.readAsArrayBuffer pour obtenir un tampon contenant les données de l’image.

Ensuite, utilisez simplement un DataView pour lire son contenu conformément à la spécification de 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 encore pire. D'autre part, vous pouvez essayer de supprimer les en-têtes PNG ou les données JPEG exif pour réduire la taille de votre image. Cela devrait être plus facile.

Vous devrez créer un autre DataWiew sur un autre tampon et le remplir avec le contenu de l'image filtrée. Ensuite, vous devrez simplement encoder le contenu de votre image dans DataURI en utilisant window.btoa.

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

3
nfroidure

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

Veuillez consulter l'exemple de TypeScript mis à jour ci-dessous, qui prend cela en compte, utilise les fonctions async et redimensionne l'image en fonction de la plus grande dimension 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;
    }

}
1
Russell Briggs

Vous pouvez jeter un oeil à conversion d'image , essayez-le ici -> page de démonstration

 enter image description here

1
王玉略

Pour la compression d’images JPG, vous pouvez utiliser la meilleure technique de compression appelée JIC (Compression d’images Javascript). Cela vous aidera certainement -> https://github.com/brunobar79/J-I-C

0
Aravind Bhat K