Redimensionner une image côté client avant l'upload


Introduction

Il y a quelques jours j'ai eu envie d'améliorer un peu 0bin afin qu'on puisse aussi partager des images en plus du code.

Et pour éviter que les gens voit une erreur "votre fichier est trop volumineux", j'ai eu envie de les redimensionner côté client avant l'upload.

Comment faire ?

En fait depuis peu, les navigateurs récent possèdent une balise HTML5 canvas que l'on peut utiliser comme un outil de traitement d'images.

La solution consiste à charger l'image dans un objet Image Javascript, puis de la dessiner redimensionnée dans un canvas. On récupère ensuite la donnée depuis le canvas et on créé un objet Blob qu'on utilise ensuite pour faire l'upload.

Voici comment on utilise le canvas pour redimensionner l'image :

var current_file = files[0];
var reader = new FileReader();
if (current_file.type.indexOf('image') == 0) {
  reader.onload = function (event) {
      var image = new Image();
      image.src = event.target.result;

      image.onload = function() {
        var maxWidth = 1024,
            maxHeight = 1024,
            imageWidth = image.width,
            imageHeight = image.height;


        if (imageWidth > imageHeight) {
          if (imageWidth > maxWidth) {
            imageHeight *= maxWidth / imageWidth;
            imageWidth = maxWidth;
          }
        }
        else {
          if (imageHeight > maxHeight) {
            imageWidth *= maxHeight / imageHeight;
            imageHeight = maxHeight;
          }
        }

        var canvas = document.createElement('canvas');
        canvas.width = imageWidth;
        canvas.height = imageHeight;
        image.width = imageWidth;
        image.height = imageHeight;
        var ctx = canvas.getContext("2d");
        ctx.drawImage(this, 0, 0, imageWidth, imageHeight);

        $('img#apercu').src = canvas.toDataURL(current_file.type);
      }
    }
  reader.readAsDataURL(current_file);
}

Et après ?

Gérer le data URL côté serveur

Ensuite vous pouvez utiliser le data url pour faire ce que vous souhaitez, l'uploader en ajax, l'afficher dans la page et si vous souhaitez enregistrer le dataUrl dans un fichier, en PHP vous feriez comme cela :

function convert_data_url($data_url) {
   // Assumes the data URL represents a JPEG image
   $image = base64_decode( str_replace('data:image/jpeg;base64,', '', $data_url);
   save_to_file($image);
}

function save_to_file($image) {
   $fp = fopen('monimage.jpg', 'w');
   fwrite($fp, $image);
   fclose($fp);
}

Vous pouvez bien évidement utiliser l'information data:image/jpeg pour sélectionner la bonne extension avec une regexp par exemple.

Gérer le data URL côté client

Vous pouvez aussi créer un FormData et faire un post du fichier Blob :

var dataURLToBlob = function(dataURL) {
    var BASE64_MARKER = ';base64,';
    if (dataURL.indexOf(BASE64_MARKER) == -1) {
      var parts = dataURL.split(',');
      var contentType = parts[0].split(':')[1];
      var raw = parts[1];

      return new Blob([raw], {type: contentType});
    }

    var parts = dataURL.split(BASE64_MARKER);
    var contentType = parts[0].split(':')[1];
    var raw = window.atob(parts[1]);
    var rawLength = raw.length;

    var uInt8Array = new Uint8Array(rawLength);

    for (var i = 0; i < rawLength; ++i) {
      uInt8Array[i] = raw.charCodeAt(i);
    }

    return new Blob([uInt8Array], {type: contentType});
};

var uploadFile = function(file) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/upload/');
    xhr.onload = function() {
        console.log(file.filename+' uploaded');
        handleComplete(file.size);
    };
    xhr.onerror = function() {
        console.log(this.responseText);
        handleComplete(file.size);
    };
    xhr.upload.onprogress = function(event) {
        handleProgress(event);
    }

    var formData = new FormData();
    formData.append('myfile', file);
    xhr.send(formData);
};

var current_file = files[0];
var reader = new FileReader();
if (current_file.type.indexOf('image') == 0) {
  reader.onload = function (event) {
      var image = new Image();
      image.src = event.target.result;

      image.onload = function() {
        var maxWidth = 1024,
            maxHeight = 1024,
            imageWidth = image.width,
            imageHeight = image.height;


        if (imageWidth > imageHeight) {
          if (imageWidth > maxWidth) {
            imageHeight *= maxWidth / imageWidth;
            imageWidth = maxWidth;
          }
        }
        else {
          if (imageHeight > maxHeight) {
            imageWidth *= maxHeight / imageHeight;
            imageHeight = maxHeight;
          }
        }

        var canvas = document.createElement('canvas');
        canvas.width = imageWidth;
        canvas.height = imageHeight;
        image.width = imageWidth;
        image.height = imageHeight;
        var ctx = canvas.getContext("2d");
        ctx.drawImage(this, 0, 0, imageWidth, imageHeight);

        // Convert the resize image to a new file to post it.
        uploadFile(dataURLToBlob(canvas.toDataURL(current_file.type)));
      }
  }
  reader.readAsDataURL(current_file);
}

Conclusion

Plus besoin de laisser votre utilisateur attendre plusieurs minutes car il essaye d'envoyer 10MP de données sur votre serveur, maintenant vous pouvez tout simplement redimensionner son image avant l'upload et même lui afficher la miniature de l'image pendant l'upload.

Is it not beautiful?

dataUrlToBlob inspiré de https://github.com/ebidel/filer.js/blob/master/src/filer.js#L128

More articles

JStorage - Cross Browser JS localStorage


Introduction

La pagination c'est bien pour l'ergonomie, mais lorsqu'il s'agit de faire des actions sur de multiples fichiers ce n'est pas forcément évident surtout si on veut conserver la sélection d'une page à l'autre.

Mon use case est de permettre à l'utilisateur de sélectionner une liste de fichiers puis de …

1 / 1