Admin/Dev

19
Janv.
2018

Optimiser une image en Javascript pour un projet PhoneGap/Cordova

Publié par sky

Aujourd'hui, travaillant sur une application mobile développée avec PhoneGap (Cordova), je suis tombé sur une problématique intéressante, et dont je vais vous partager la solution.

Lors d'un événement interne d'une société rassemblant beaucoup de monde, et plus particulièrement lors d'une session de "team-building",  il y avait la nécessité à un moment donné, pour chacun des participants d'uploader une photo vers un serveur distant (Apache / PHP).

La problématique qui nous a rapidement sauté aux yeux, est que les photos prises par les portables sont désormais très lourdes, la qualité et la résolution s'améliorant à chaque nouvelle génération. Hors si, à un instant T, ce sont 200 personnes qui uploadent une image de 7 ou 8 Mo vers le serveur, il risque d'y avoir encombrement, déjà côté serveur pour enregistrer les images, mais surtout sur la très faible connexion internet du lieu de l'événement. Imaginons que cette connexion soit déjà très très efficace, disposant d'un débit montant de 1Mo/s (ce dont je doute), tel quel, il faudrait près de 30 minutes pour uploader toutes les photos, tout le monde aurait le temps de s'endormir avant la fin de la session !

Il était donc nécessaire de trouver une solution à ce problème. Sachant que le point faible est cette connexion montante, il fallait trouver une solution en amont, du coup, il n'y avait pas le choix, il fallait trouver une solution au niveau du mobile. Et cette solution est finalement simple, il suffit de réduire la taille et donc le poids des images avant l'envoi vers le serveur.

Voici un pas-à-pas pour mettre en place cette fonctionnalité.

Prenons le code de base d'une application PhoneGap :

var app = {
// Application Constructor initialize: function() {
this.bindEvents(); },      // Bind Event Listeners // // Bind any events that are required on startup. Common events are: // 'load', 'deviceready', 'offline', and 'online'. bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false); },      // deviceready Event Handler // // The scope of 'this' is the event. In order to call the 'receivedEvent' // function, we must explicitly call 'app.receivedEvent(...);' onDeviceReady: function() {              }, };

Puis demandons à l'application d'accéder à l'appareil photo, cela se passe dans la méthode onDeviceReady que voici :

onDeviceReady: function() {        
  navigator.camera.getPicture(onTakePictureSuccess, onTakePictureFail, {
  quality: 100,   destinationType: Camera.DestinationType.DATA_URL,     correctOrientation: true   }); },

Si vous n'êtes pas familier avec ce code, nous avons indiqué :

  • la méthode gérant le résultat en cas de succès : onTakePictureSuccess
  • la méthode gérant le résultat en cas d'échec : onTakePictureFail
  • les paramètres qui sont :
    • la qualité
    • le type de résultat souhaité
    • la correction de l'orientation (nécessaire pour les iPhones)

Nous devons donc créer nos deux méthodes "handler", qui se trouve en dehors de l'objet app.

function onTakePictureSuccess(imageData) {
  app.preparePicture(imageData);
}   function onTakePictureFail(message) {
  console.log('Capture echouee - Erreur : ' + message);
}

Lors de la réussite, nous récupérons le contenu de l'image, au format Camera.DestinationType.DATA_URL, dans la variable imageData, que nous réinjectons dans l'application avec une nouvelle méthode à créer. En cas d'échec, nous affichons simplement le problème survenu.

Et c'est ici que tout cela devient intéressant, puisque c'est ici que nous devons compresser l'image avant de l'envoyer vers le serveur.

Pour commencez, créons la méthode (devant se trouver dans l'application, si vous suivez bien)

preparePicture: function(imageData) {
    /* affichons poids l'image en entree */   console.log (imageData.length/1024+'ko');   },

dans laquelle nous affichons le poid de l'image en entrée.

Puis chargeons l'image dans un Objet HTML de type Image

preparePicture: function(imageData) {
    /* affichons poids l'image en entree */   console.log (imageData.length);     /* creons une image de la photo */   image = new Image();   image.src = 'data:image/jpeg;base64,'+imageData; },

vérifions ensuite lorsque l'image est totalement chargée

preparePicture: function(imageData) {
    /* affichons poids l'image en entree */   console.log (imageData.length/1024+'ko');     /* creons une image de la photo */   image = new Image();   image.onload = function () {
    }   image.src = 'data:image/jpeg;base64,'+imageData; },

dans lequel nous allons mettre ce fameux que, visiblement, vous attendez, si vous avez lu jusqu'ici.

Le principe est d'utiliser l'élément HTML Canvas, qui permet de manipuler des images, et dans notre cas, qui permet de les redimensionner.

Canvas est un objet, à la base, créé par Apple dans WebKit pour gérer quelques interfaces de Mac OS X, qui a depuis été ajouté à Safari, et porté par d'autres moteurs de rendu HTML. Il est nécessaire d'avoir Firefox 2 ou Internet Explorer 9 pour en profiter dans un navigateur internet. Mais ici, la question ne se pose pas, avec Cordova, c'est exclusivement le WebKit qui est utilisé.

Pour cela, il faut définir le canvas à la taille finale de l'image. A vous de voir quelle taille minimale vous aurez besoin en sortie, dans mon exemple, je vais limiter la taille à 1024 pixels de large ou 800 pixels de haut.

preparePicture: function(imageData) {
    /* affichons poids l'image en entree */   console.log (imageData.length/1024+'ko');     /* creons une image de la photo */   image = new Image();   image.onload = function () {
    /* creons notre canvas de travail */     canvas = document.createElement('canvas');          /* dimensionnons notre canvas */         if (image.width > image.height) {
          canvas.width = 1024;           canvas.height = 1024 * (image.height / image.width);         } else {
          canvas.height = 800;           canvas.width = 800 * (image.width / image.height);         }   }   image.src = 'data:image/jpeg;base64,'+imageData; },

enfin, nous allons redimensionner l'image dans le canvas, puis récupérer le résultat pour l'envoyer en POST dans votre méthode de connexion avec le serveur, en oubliant d'afficher dans la console, le poids de l'image en sortie, et ainsi voir le gain.

preparePicture: function(imageData) {
 
  /* affichons poids l'image en entree */
  console.log (imageData.length/1024+'ko');
 
  /* dimensionnons notre canvas */
  image = new Image();
  image.onload = function () {
    /* creons notre canvas de travail */
    canvas = document.createElement('canvas');
    
    /* dimensionnons notre canvas */
        if (image.width > image.height) {
          canvas.width = 1024;
          canvas.height = 1024 * (image.height / image.width);
        } else {
          canvas.height = 800;
          canvas.width = 800 * (image.width / image.height);
        }
        
        /* creons le contexte de notre canvas */
        ctx = canvas.getContext('2d');
        
        /* utilisons le context pour copier l'image dans le canvas */
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
            
        /* recuperons le contenu du canvas en oubliant de supprimer l'information 'data:image/jpeg;base64,' devenu inutile */
        newImageData = canvas.toDataURL('image/jpeg').substring(23);
        
        /* affichons le poids en sortie */
        console.log (newImageData.length/1024+'ko');
      
        /* inserer ici votre methode d'envoi vers le serveur */
        this.sendToServeur({'imageData':newImageData});
  }
  image.src = 'data:image/jpeg;base64,'+imageData;
},


Voici le code complet

var app = {

    // Application Constructor
    initialize: function() {
        this.bindEvents();
    },
    
    // Bind Event Listeners
    //
    // Bind any events that are required on startup. Common events are:
    // 'load', 'deviceready', 'offline', and 'online'.
    bindEvents: function() {
        document.addEventListener('deviceready', this.onDeviceReady, false);
    },
    
    // deviceready Event Handler
    //
    // The scope of 'this' is the event. In order to call the 'receivedEvent'
    // function, we must explicitly call 'app.receivedEvent(...);'
    onDeviceReady: function() {        
        navigator.camera.getPicture(onTakePictureSuccess, onTakePictureFail, {
        quality: 100,
        destinationType: Camera.DestinationType.DATA_URL,
        correctOrientation: true
      });
    },
    
    preparePicture: function(imageData) {
 
  /* affichons poids l'image en entree */
  console.log (imageData.length/1024+'ko');
 
  /* dimensionnons notre canvas */
  image = new Image();
  image.onload = function () {
    /* creons notre canvas de travail */
    canvas = document.createElement('canvas');
    
    /* dimensionnons notre canvas */
        if (image.width > image.height) {
          canvas.width = 1024;
          canvas.height = 1024 * (image.height / image.width);
        } else {
          canvas.height = 800;
          canvas.width = 800 * (image.width / image.height);
        }
        
        /* creons le contexte de notre canvas */
        ctx = canvas.getContext('2d');
        
        /* utilisons le context pour copier l'image dans le canvas */
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
            
        /* recuperons le contenu du canvas en oubliant de supprimer l'information 'data:image/jpeg;base64,' devenu inutile */
        newImageData = canvas.toDataURL('image/jpeg').substring(23);
        
        /* affichons le poids en sortie */
        console.log (newImageData.length/1024+'ko');
      
        /* inserer ici votre methode d'envoi vers le serveur */
        this.sendToServeur({'imageData':newImageData});
  }
  image.src = 'data:image/jpeg;base64,'+imageData;
},
};

function onTakePictureSuccess(imageData) {
  app.preparePicture(imageData);
}   function onTakePictureFail(message) {
  console.log('Capture echouee - Erreur : ' + message);
}

Le résultat est plutôt bon, puisque de 7 à 8Mo en entrée, nous finissons à 150Ko en sortie. En reprenant notre connexion à 1Mo/s, il faudrait désormais moins d'une minute pour envoyer l'ensemble des photos.

Du côté serveur, le code est extrèment simple, puisqu'il suffit de récupérer le contenu de la variable pour la copier dans un nouveau fichier JPEG. Si vous souhaitez vous assurer qu'il s'agit bien d'une image, vous pouvez utiliser GD pour écrire votre image au format que vous souhaitez.

Voici une base de code à modifier pour ajouter les contrôles nécessaires, et compléter avec vos besoins :

/*  je defini le chemin ou doit être stocke le fichier final, ici au format JPEG */
$path = 'path/to/file.jpg'

/* je code l'image envoyee au format base64 */
$imageData = base64_decode($_REQUEST['imageData']);

/* je cree une image a partir du contenu de l'image */
$source = imagecreatefromstring($imageData);

/* j'enregistre mon image en qualite maximal (JPEG uniquement) */
imagejpeg($source, $path, 100);

/* je supprime l'image temporaire de la memoire */
imagedestroy($source);

Et voila, le tour est joué. En espérant que cette solution vous sera utile un jour.

 
 
Commentaires
Aucun commentaire pour le moment.

 

Poster un commentaire
En postant sur skymac.org, je m'engage à être courtois et à ce que mon message soit pertinent avec le sujet de l'article.
En outre, j'accepte, sans condition, que mon message soit refusé et supprimé si ces règles ne sont pas appliquées.
Les cookies assurent le bon fonctionnement de nos services. En continuant, vous acceptez leur utilisation sur notre site internet.
Accepter