HTML5 Pré-redimensionner les images avant de les télécharger

181

Voici un grattoir à nouilles.

Gardant à l'esprit que nous avons le stockage local HTML5 et xhr v2 et autres. Je me demandais si quelqu'un pouvait trouver un exemple de travail ou même simplement me donner un oui ou un non à cette question:

Est-il possible de pré-dimensionner une image en utilisant le nouveau stockage local (ou autre), de sorte qu'un utilisateur qui n'a pas la moindre idée du redimensionnement d'une image puisse faire glisser son image 10 Mo dans mon site Web, il la redimensionner en utilisant le nouveau stockage local et ALORS téléchargez-le à la plus petite taille.

Je sais très bien que vous pouvez le faire avec Flash, applets Java, X actif ... La question est de savoir si vous pouvez le faire avec Javascript + Html5.

Dans l'attente de la réponse sur celui-ci.

Ta pour l'instant.

Jimmyt1988
la source

Réponses:

181

Oui, utilisez l' API File , vous pouvez alors traiter les images avec l' élément canvas .

Cet article de blog Mozilla Hacks vous guide tout au long du processus. Pour référence, voici le code source assemblé de l'article de 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
robertc
la source
6
Pourquoi merci bon monsieur. Je vais jouer ce soir ... Avec le fichier api qui est. J'ai fait fonctionner le téléchargement par glisser-déposer et j'ai réalisé que ce serait également une fonctionnalité très intéressante à inclure. Hourra.
Jimmyt1988
3
N'avons-nous pas besoin d'utiliser cette mycanvas.toDataURL ("image / jpeg", 0.5) pour nous assurer que la qualité est apportée afin de diminuer la taille du fichier. Je suis tombé sur ce commentaire sur le billet de blog Mozilla et j'ai vu la résolution du bogue ici bugzilla.mozilla.org/show_bug.cgi?id=564388
sbose
33
Vous devez attendre onloadsur l'image (attacher le gestionnaire avant de définir src), car le navigateur est autorisé à effectuer le décodage de manière asynchrone et les pixels peuvent ne pas être disponibles avant drawImage.
Kornel
6
Vous voudrez peut-être mettre le code du canevas dans la fonction onload du filereader - il est possible que de grandes images ne soient pas chargées avant ctx.drawImage()la fin.
Greg
4
attention, vous avez besoin de magie supplémentaire pour le faire fonctionner sur IOS: stackoverflow.com/questions/11929099/…
flo850
107

J'ai abordé ce problème il y a quelques années et téléchargé ma solution sur github sous le nom https://github.com/rossturner/HTML5-ImageUploader

La réponse de robertc utilise la solution proposée dans le billet de blog Mozilla Hacks , mais j'ai trouvé que cela donnait une très mauvaise qualité d'image lors du redimensionnement à une échelle qui n'était pas 2: 1 (ou un multiple de celle-ci). J'ai commencé à expérimenter avec différents algorithmes de redimensionnement d'image, bien que la plupart aient fini par être assez lents ou bien de mauvaise qualité non plus.

Enfin, j'ai trouvé une solution qui, je pense, s'exécute rapidement et offre également de très bonnes performances - car la solution Mozilla de copie d'une toile à une autre fonctionne rapidement et sans perte de qualité d'image à un rapport 2: 1, étant donné un objectif de x pixels de large et y pixels de haut, j'utilise cette méthode de redimensionnement de la toile jusqu'à ce que l'image soit comprise entre x et 2 x , et y et 2 y . À ce stade, je passe ensuite au redimensionnement algorithmique de l'image pour la dernière "étape" de redimensionnement jusqu'à la taille cible. Après avoir essayé plusieurs algorithmes différents je me suis installé sur une interpolation bilinéaire tirée d'un blog qui n'est plus en ligne mais accessible via Internet Archive , ce qui donne de bons résultats, voici le code applicable:

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

Cela réduit une image à une largeur de config.maxWidth, en conservant le rapport hauteur / largeur d' origine. Au moment du développement, cela fonctionnait sur iPad / iPhone Safari en plus des principaux navigateurs de bureau (IE9 +, Firefox, Chrome), donc je pense qu'il sera toujours compatible compte tenu de l'adoption plus large de HTML5 aujourd'hui. Notez que l'appel canvas.toDataURL () prend un type mime et une qualité d'image qui vous permettront de contrôler la qualité et le format du fichier de sortie (potentiellement différent de l'entrée si vous le souhaitez).

Le seul point que cela ne couvre pas est de conserver les informations d'orientation, sans connaissance de ces métadonnées, l'image est redimensionnée et enregistrée telle quelle, perdant toutes les métadonnées de l'image pour l'orientation, ce qui signifie que les images prises sur une tablette "à l'envers" étaient rendus comme tels, bien qu'ils auraient été retournés dans le viseur de l'appareil photo. Si cela est un problème, cet article de blog contient un bon guide et des exemples de code sur la façon d'accomplir cela, qui, j'en suis sûr, pourraient être intégrés au code ci-dessus.

Ross Taylor-Turner
la source
4
Je vous ai décerné la prime parce que je pense que cela ajoute beaucoup à la réponse, je dois cependant intégrer ces éléments d'orientation :)
Sam Saffron
3
Une âme très utile a maintenant fusionné dans certaines fonctionnalités pour les métadonnées d'orientation :)
Ross Taylor-Turner
26

Correction ci-dessus:

<img src="" id="image">
<input id="input" type="file" onchange="handleFiles()">
<script>

function handleFiles()
{
    var filesToUpload = document.getElementById('input').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;

        var canvas = document.createElement("canvas");
        //var canvas = $("<canvas>", {"id":"testing"})[0];
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0);

        var MAX_WIDTH = 400;
        var MAX_HEIGHT = 300;
        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");
        document.getElementById('image').src = dataurl;     
    }
    // Load files into file reader
    reader.readAsDataURL(file);


    // Post the data
    /*
    var fd = new FormData();
    fd.append("name", "some_filename.jpg");
    fd.append("image", dataurl);
    fd.append("info", "lah_de_dah");
    */
}</script>
Justin Levene
la source
2
Comment gérez-vous la partie PHP après l'avoir ajoutée à FormData ()? Vous ne chercheriez pas $ _FILES ['name'] ['tmp_name'] [$ i], par exemple? J'essaye si (isset ($ _ POST ['image'])) ... mais dataurlpas là.
denikov
2
cela ne fonctionne pas sur Firefox la première fois que vous essayez de télécharger une image. La prochaine fois que vous essayez, cela fonctionne. Comment le réparer?
Fred
retourne si le fichier est tableau est nul ou vide.
Sheelpriy
2
Dans Chrome lors de la tentative d'accès img.widthet au img.heightdépart, ils le sont 0. Vous devriez mettre le reste de la fonction à l'intérieurimg.addEventListener('load', function () { // now the image has loaded, continue... });
Inigo
2
@Justin pouvez-vous clarifier "ce qui précède", comme dans, de quoi cette publication est-elle une correction? Cheers
Martin
16

Modification de la réponse de Justin qui fonctionne pour moi:

  1. Ajout de img.onload
  2. Développez la requête POST avec un exemple réel

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);
}
Volonté
la source
2
information d'orientation (exif) se perd
Konstantin Vahrushev
8

Si vous ne voulez pas réinventer la roue, vous pouvez essayer plupload.com

nanospeck
la source
3
J'utilise également plupload pour le redimensionnement côté client. La meilleure chose à ce sujet est qu'il peut utiliser flash, html5, silverlight, etc. Tout ce que l'utilisateur a à disposition, car chaque utilisateur a une configuration différente. Si vous ne voulez que du HTML5, alors je pense que cela ne fonctionnera pas sur certains anciens navigateurs et Opera.
jnl
6

Manuscrit

async resizeImg(file: Blob): Promise<Blob> {
    let img = document.createElement("img");
    img.src = await new Promise<any>(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<Blob>(resolve => { canvas.toBlob(resolve, 'image/jpeg', 0.95); });
    return result;
}
jales cardoso
la source
1
Quiconque rencontre ce post, cette réponse basée sur la promesse est beaucoup plus fiable à mon avis. Je me suis converti en JS normal et cela fonctionne comme un charme.
gbland777
5
fd.append("image", dataurl);

Cela ne fonctionnera pas. Du côté PHP, vous ne pouvez pas enregistrer de fichier avec cela.

Utilisez plutôt ce code:

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
robot
la source
5

Le redimensionnement des images dans un élément de canevas est généralement une mauvaise idée car il utilise l'interpolation de boîte la moins chère. L'image résultante se dégrade en qualité. Je recommanderais d'utiliser http://nodeca.github.io/pica/demo/ qui peut effectuer la transformation Lanczos à la place. La page de démonstration ci-dessus montre la différence entre les approches canvas et Lanczos.

Il utilise également des web workers pour redimensionner les images en parallèle. Il existe également une implémentation WEBGL.

Il existe des redimensionneurs d'images en ligne qui utilisent pica pour faire le travail, comme https://myimageresizer.com

Ivan Nikitine
la source
5

La réponse acceptée fonctionne très bien, mais la logique de redimensionnement ignore le cas où l'image est plus grande que le maximum dans un seul des axes (par exemple, height> maxHeight mais width <= maxWidth).

Je pense que le code suivant prend en charge tous les cas de manière plus simple et fonctionnelle (ignorez les annotations de type dactylographié si vous utilisez du javascript brut):

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 };
  }
Taro
la source
0

Vous pouvez utiliser dropzone.js si vous souhaitez utiliser un gestionnaire de téléchargement simple et facile avec des fonctions de redimensionnement avant téléchargement.

Il a des fonctions de redimensionnement intégrées, mais vous pouvez fournir les vôtres si vous le souhaitez.

Michał Zalewski
la source