Transformations élémentaires sur images dans Canvas

Inversion, rotation à 90 degrés, agrandissement, les opérations de base sur les images dans HTML 5.

Pour ces opérations de base nous allons utiliser les fonctions intégrées à l'extension JavaScript pour canvas, mais aussi une routine qui lit le contenu d'une image pixel par pixel et la reproduit dans le canvas. Cette fonction, contrairement aux fonctions intégrées, permet si on le veut de créer ses propres effets, notamment sur les couleurs.
Par exemple on pourrait réduire le niveau de transparence en partant de la gauche vers droite, si on voulait fondre une image dans une autre comme on le fait avec Gimp. Ce n'est pas le sujet de cet article, mais je préciserais quand même que cela peut se faire en modifiant la valeur alpha dans le code de couleur RVBA (rouge, vert, bleu, alpha).

L'image que nous allons utiliser à titre de démonstration pour tester les transformations est ci-dessous. Ces voiliers sur une mer bleue (la méditérranée) vont subir toutes sortes de manipulations par des fonctions dont nous allons donner le code.

Agrandissement avec la fonction scale

La fonction scale permet de changer la taille de l'image et la routine suivante en montre l'utilisation: on charge une image dans une balise img puis on modifie l'affichage dans une balise canvas en spécifiant un facteur d'agrandissement. Tout ce qui est affiché ensuite le sera avec cet agrandissement, et cela s'appliquera donc à la fonction drawImage utilisée pour afficher notre image. La taille de la balise canvas est aussi définie en fonction du facteur d'agrandissement.

<canvas id="canvasid"></canvas>
function transform(canvasid, filename, scale) {
  var viewCanvas = document.getElementById(canvasid);
  var viewCtx = viewCanvas.getContext("2d");
  var imageSource = new Image();
  imageSource.onload  = function () {
    viewCanvas.width = imageSource.width * scale;
    viewCanvas.height = imageSource.height  * scale;				
    viewCtx.scale(scale,scale);
    viewCtx.drawImage(imageSource, 0, 0);
  }
  imageSource.src = filename;
}

transform("canvasid", "image.jpg", 2);

Agrandissement avec un algorithme JavaScript

On veut lire l'image pixel par pixel et la reproduire de la même façon, en agrandissant les pixels, on doit donc stocker l'image originale dans une balise canvas temporaire qui n'est pas affichée. Une seconde balise est utilisée pour l'image transformée.
La fonction drawImage affiche l'image dans le canvas caché après son chargement. La fonction getImage la transforme en matrice de points dans laquelle on peut accéder aux pixels individuellement ce que fait la fonction getColor. On construit alors une propriété de couleur au format rgba et on l'assigne à un rectangle qui représente le pixel avec éventuellement un facteur d'agrandissement donné.

<canvas id="temporary" style="display:none"></canvas>
<canvas id="canvasid"></canvas>

function getColor(imgdata, x, y, width) {
  var index = (x + y * width) * 4;
  r = imgdata.data[index + 0];
  g = imgdata.data[index + 1];
  b = imgdata.data[index + 2];
  a = imgdata.data[index + 3];
  return 'rgba(' + r + ',' +  g + ',' +  b + ',' +  a + ')';
}

function transform(canvasid, filename, scale) {
  var i, j, it, jt;
  var tempCanvas = document.getElementById("temporary");
  var tempCtx = tempCanvas.getContext("2d");
  var viewCanvas = document.getElementById(canvasid);
  var ctxtarget = canvasTarget.getContext("2d");

  var imageSource = new Image();
  imageSource.onload  = function () {
    var w = imageSource.width;
    var h = imageSource.height;
    tempCanvas.width = w;
    tempCanvas.height = h;
    tempCtx.drawImage(imageSource, 0, 0, w, h);
    viewCanvas.width = w * scale;
    viewCanvas.height = h  * scale;
    var sourceData = tempCtx.getImageData(0, 0, w, h );
    ctxtarget.beginPath();
    for (i = 0; i < w; i++) {
      it = i * scale;
      for (j = 0; j < h; j++) {
        jt = j * scale;
        var color = getColor(sourceData, i, j, w);
        ctxtarget.fillStyle = color;
        ctxtarget.fillRect(it, jt, scale, scale);	
      }
    }
  }
  imageSource.src = filename;
}

transform("canvasid", "image.jpg", 2);

La qualité n'est pas très différente dans ce cas de figure mais ce n'est pas toujours le cas. Comme on le verra dans le comparatif des outils de super résolution (lien en bas de page), l'agorithme d'interpolation utilisée par canvas peut améliorer le rendu. Mais notre routine elle permet de contrôler si besoin chaque pixel de l'image.

Inversion horizontale

Pour afficher l'image inversée, donc de droite à gauche, la même fonction que pour l'agrandissement est utilisé avec une boucle modifiée:

var it = 0;
for (i = w - 1; i >= 0; i--) {
  for (j = 0; j < h; j++) {
    ctxtarget.fillStyle = getColor(sourceData, it, j, w);
    ctxtarget.fillRect(i, j, 1, 1);	
  }
  it++;
}	

L'image est scannée de droite à gauche. On ne positionne pas le pixel dans la nouvelle image avec la même coordonnée horizontale que l'image source, mais avec une valeur que l'on incrémente à partir de zéro.

Inversion horizontale avec les fonctions intégrées

Il n'existe pas de fonction flip dans canvas, mais le même effet peut être obtenu en modifiant la façon dont on affiche le contenu, cela avec les fonctions translate et scale.

function hflipBuiltin(canvasTarget, image, w, h) {
  canvasTarget.width = w;
  canvasTarget.height = h;
  var ctxtarget = canvasTarget.getContext("2d");   
  ctxtarget.translate(w, 0);
  ctxtarget.scale(-1, 1);
  ctxtarget.drawImage(image, 0, 0);
}

Le code est très simplifié par rapport a notre fonction d'inversion. On fait une translation de coordonnée horizontale pour commencer l'affichage sur la droite. Puis on donne une valeur négative à scale pour afficher de droite à gauche. Ne reste plus qu'à utiliser la fonction drawImage pour afficher l'image.

Inversion verticale

Pour tirer profit de cette transformation, il faudrait que l'image soit elle-même inversée. On peut alors construire une scène en partant du bas et non plus de haut en bas comme le fait canvas par défaut. Pour réaliser un jeu par exemple, ce serait plus intuitif.

On remplace la boucle dans l'algo précédent par celle-ci:

for (i = 0; i < w; i++) {
  var jt = 0;
  for (j = h + 1; j >= 0; j--) {
    ctxtarget.fillStyle = getColor(sourceData, i, jt, w);
    ctxtarget.fillRect(i, j, 1, 1);	
    jt++;
  }
}	

Si on n'a aucune modification à faire sur l'image autre que l'inversion, on peut aussi utiliser la même fonction simplifiée que pour l'inversion horizontale, avec ces deux lignes modifiées:

ctxtarget.translate(0, h);
ctxtarget.scale(1, -1); 

Rotation de 90° dans le sens des aiguilles d'une montre

Le calcul des nouvelles dimensions est simplifié dans ce cas de figure: on intervertit la hauteur et la longueur dans le cas d'un rectangle. On ne change rien pour un carré.

Il faut effectuer une translation de l'origine pour tenir compte de son déplacement. Elle correspond à la hauteur de l'image.

function rotate90(canvasTarget, image, w, h) {
  canvasTarget.width = h;
  canvasTarget.height = w;
  var ctxtarget = canvasTarget.getContext("2d");   
  ctxtarget.translate(h, 0);
  ctxtarget.rotate(Math.PI / 2);  
  ctxtarget.drawImage(image, 0, 0);  
}

Notez qu'on a assigné à la largeur du canvas la hauteur de l'image et à la hauteur de canvas la largeur de l'image.

La fonction rotate à pour effet de décaler l'image. Pour contrer ce décalage, on fait une translation horizontale équivalente à la hauteur de l'image. Cela convient pour une rotation de 90° uniquement.

Rotation de 90° en sens inverse

Cette fois la translation de coordonnée se fait sur la base de la largeur de l'image. On utilise la même fonction que précédemment, en changeant juste cette ligne:

ctxtarget.translate(0, w);

Le code source complet de toutes ces fonctions avec en plus un shell de commandes. Ce code est utilisable dans vos projets pourvu que la notice de copyright soit préservée.

Pour agrandir des images avant de les placer dans canvas, ou hors de canvas, des outils mettent en oeuvre des algorithmes étonnants, dont voici une sélection...