Dessinez avec le paquet image/draw de la bibliothèque standard

Cet article est une traduction de la page The Go image/draw package (en anglais sur le site du projet Go).

Introduction

Le paquet image/draw (en anglais) ne définit qu'une opération : dessiner une image source dans une image de destination par l'intermédiaire d'une image masque optionnelle. Cette seule opération est pourtant étonnamment flexible, car elle peut accomplir élégamment et efficacement plusieurs tâches de manipulation d'image.

La composition est réalisée pixel par pixel dans le style de la bibliothèque graphique du système d'exploitation Plan 9 et son extension X Render. Ce modèle est basé sur l'article Compositing Digital Images par Porter et Duff, avec un paramètre « masque » supplémentaire : dst = (src IN mask) OP dst. Pour une image masque complètement opaque, cela réduit la formule originelle de Porter-Duff à : dst = src OP dst. En Go, une image masque égal à nil est équivalent à une image masque d'une taille infinie et complètement opaque.

L'article de Porter et Duff présentait 12 opérateurs de composition différents (en anglais), mais avec un masque explicite, seuls deux d'entre eux sont nécessaires dans la pratique : source-sur-destination et source. En Go, ces opérateurs sont représentés par les constantes Over et Src. L'opérateur Over réalise la superposition naturelle d'une image source sur une image de destination : le changement sur l'image de destination est plus léger lorsque l'image source (après application du masque) est plus transparente (c.-à-d. que son canal alpha a une valeur plus petite). L'opérateur Src copie simplement la source (après application du masque) sans s'occuper du contenu originel de l'image de destination. Pour les images source et de masque qui sont complètement opaques, les deux opérateurs produisent le même résultat, mais l'opérateur Src est en général le plus rapide.

Alignement géométrique

La composition nécessite d'associer les pixels de destination avec les pixels source et de masque. Évidemment, cela nécessite des images source, destination et masque ainsi qu'un opérateur de composition. Mais cela requiert également de spécifier quel rectangle il faut utiliser dans chacune des images. Il n'est pas forcément nécessaire d'écrire l'intégralité de la destination à chaque fois que l'on dessine : lorsque l'on met à jour une image animée, il est plus efficace de dessiner seulement les parties de l'image qui ont changé. Tout comme il n'est pas forcément nécessaire de lire l'intégralité de la source à chaque fois que l'on dessine : lorsque l'on utilise un sprite combinant plusieurs petites images dans une grande image (NDT: mosaïque d'images regroupant des images indépendantes les unes des autres en une seule grande image. Cette technique est souvent utilisée pour réduire la taille globale des images ou le nombre d'opérations ou d'appels serveur : on ne charge qu'une image au lieu de plusieurs images), seule une partie de l'image est nécessaire. Il n'est pas nécessaire de lire l'intégralité de l'image masque à chaque fois que l'on dessine : une image de masque qui collecte les glyphes d'une fonte (NDT: caractères d'une police de caractères) est la même chose qu'un sprite. Ainsi, dessiner nécessite de connaître trois rectangles, un pour chacune des images. Étant donné que chaque rectangle a la même longueur et la même hauteur, il suffit de passer en paramètre un rectangle de destination r ainsi que deux points sp et mp : le rectangle source est égal à r transposé de manière à ce que r.Min dans l'image de destination s'aligne avec sp dans l'image source et de même pour mp. Le rectangle résultant est aussi redimensionné de manière à ce qu'il corresponde aux dimensions des images source dans leurs espaces de coordonnées respectifs.

  • Exemple de manipulations basiques

    Exemple de manipulations basiques

La fonction DrawMask (en anglais) accepte sept paramètres, mais un masque explicite et un masque-point ne sont en général pas nécessaires, ainsi la fonction Draw (en anglais) accepte cinq paramètres :

// Draw appelle la fonction DrawMask avec un masque égal à nil.
func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)

func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point,
				mask image.Image, mp image.Point, op Op)

L'image de destination doit pouvoir muter, donc le paquet image/draw définit une interface draw.Image (en anglais) qui a une méthode Set.

type Image interface {
    image.Image
    Set(x, y int, c color.Color)
}

Remplir un rectangle

Pour remplir un rectangle avec une couleur unie, utilisez une image source de type image.Uniform. Le type ColorImage réinterprète une couleur (type Color) comme une image (type Image) de cette couleur et qui a pratiquement une taille infinie. Pour ceux qui connaissent la bibliothèque draw du système d'exploitation Plan 9, il n'y a pas besoin d'utiliser explicitement repeat bit avec les types d'images basées sur des slices. En Go, ce concept est induit lorsque l'on utilise le type Uniform.

// image.ZP est le point de départ, l'origine.
draw.Draw(dst, r, &image.Uniform{c}, image.ZP, draw.Src)

Pour initialiser une nouvelle image entièrement bleue :

m := image.NewRGBA(image.Rect(0, 0, 640, 480))
blue := color.RGBA{0, 0, 255, 255}
draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src)

Pour initialiser une image de manière à ce qu'elle soit transparente (ou noire si le modèle de couleur de l'image de destination ne peut pas représenter la transparence), utilisez la valeur image.Transparent qui est de type image.Uniform :

draw.Draw(m, m.Bounds(), image.Transparent, image.ZP, draw.Src)
  • Exemple d'initialisation à transparent

    Exemple d'initialisation à transparent

Copier une image

Pour copier le rectangle sr d'une image source vers un rectangle commençant à un point dp dans une image de destination, convertissez le rectangle source dans l'espace de coordonnées de l'image de destination :

r := image.Rectangle{dp, dp.Add(sr.Size())}
draw.Draw(dst, r, src, sr.Min, draw.Src)

On peut aussi écrire :

r := sr.Sub(sr.Min).Add(dp)
draw.Draw(dst, r, src, sr.Min, draw.Src)

Pour copier toute l'image source, utilisez sr = src.Bounds().

  • Exemple de copie intégrale d'une image

    Exemple de copie intégrale d'une image

Faire défiler une image

Faire défiler une image consiste juste à copier une image vers elle-même, avec des rectangles source et destination différents. Faire chevaucher les images destination et source est parfaitement valide, tout comme la fonction native copy de Go peut gérer le chevauchement des slices de destination et de source. Donc, pour faire défiler de 20 pixels une image m, on écrira :

b := m.Bounds()
p := image.Pt(0, 20)
// Notez que même si le second paramètre est b, 
// Le rectangle résultant est plus petit à cause du rognage (clipping).
draw.Draw(m, b, m, b.Min.Add(p), draw.Src)
dirtyRect := b.Intersect(image.Rect(b.Min.X, b.Max.Y-20, b.Max.X, b.Max.Y))
  • Exemple de défilement d'une image

    Exemple de défilement d'une image

Convertir une image vers le format RGBA

Le résultat du décodage d'une image peut ne pas être au format image.RGBA. Par exemple, décoder une image GIF a pour résultat une image au format image.Paletted ; le décodage d'une image JPEG est stocké dans le format ycbcr.YCbCr ; et pour une image PNG, tout dépend des données de l'image. Voici le code source permettant de convertir toute image source dans le format image.RGBA :

b := src.Bounds()
m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(m, m.Bounds(), src, b.Min, draw.Src)
  • Exemple de décodage de format

    Exemple de décodage de format

Dessiner à travers un masque

Pour dessiner une image à travers un masque circulaire avec un centre p et un rayon r, on utilisera le code suivant :

type circle struct {
    p image.Point
    r int
}

func (c *circle) ColorModel() color.Model {
    return color.AlphaModel
}

func (c *circle) Bounds() image.Rectangle {
    return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
}

func (c *circle) At(x, y int) color.Color {
    xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r)
    if xx*xx+yy*yy < rr*rr {
        return color.Alpha{255}
    }
    return color.Alpha{0}
}

draw.DrawMask(dst, dst.Bounds(), src, image.ZP, &circle{p, r}, image.ZP, draw.Over)
  • Exemple de dessin à travers un masque

    Exemple de dessin à travers un masque

Dessiner des glyphes d'une police de caractère

Pour dessiner un caractère en bleu et à partir du point p, dessinez avec une source image.ColorImage et un masque image.Alpha. Par souci de simplicité, Go n'effectue pas de rendu ou de positionnement au sous-pixel. De même, Go ne corrige pas la hauteur d'une police au-dessus d'une ligne de base (NDT: voir l'article de ce blog : écrire du texte dans une image pour une solution complète intégrant le moteur de rendu de police de caractères freetype).

src := &image.Uniform{color.RGBA{0, 0, 255, 255}}
mask := theGlyphImageForAFont()
mr := theBoundsFor(glyphIndex)
draw.DrawMask(dst, mr.Sub(mr.Min).Add(p), src, image.ZP, mask, mr.Min, draw.Over)
  • Exemple d'utilisation d'une police de caractères

    Exemple d'utilisation d'une police de caractères

Performance

L'implémentation du paquet image/draw démontre comment fournir une fonction de manipulation d'image qui est à la fois générique, mais également efficace dans les cas d'usage les plus courants. La fonction DrawMask accepte des paramètres de type interface, mais effectue immédiatement une assertion de type afin de vérifier que les paramètres reçus correspondent bien à certains types de structure. Les types attendus doivent correspondre aux types des opérations courantes telles que dessiner une image de type image.RGBA sur une autre ; ou dessiner un masque de type image.Alpha (un caractère, par exemple) sur une image de type image.RGBA. Si l'assertion de type fonctionne, cela permet d'exécuter une implémentation spécialisée de l'algorithme général (en fonction du type détecté). Si l'assertion de type échoue, le chemin de code alternatif utilise les méthodes génériques At et Set. Ces chemins rapides d'exécution (NDT: fast-paths) sont seulement une optimisation pour une exécution plus rapide ; l'image de destination résultante est la même de toute façon. Dans la pratique, seul un petit nombre de cas particuliers sont nécessaires à la plupart des applications.


Étiquettes :   image 
Portrait de Benjamin BALET
Benjamin BALET
Consultant APM

Retrouvez mes cooordonées

Benjamin BALET sur viadeo






Vous aimerez aussi

Les lois de la réflexion

Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go.   Lire »

Gobs le format natif d'échange de données en Go

Traduction d'un article du blog officiel expliquant comment échanger des données entre deux programmes golang grâce à un format natif   Lire »

Comment écrire du code Go ?

Traduction d'une partie des spécifications officielles du langage Go, cet article explique comment développer en Go.   Lire »

Le modèle mémoire du runtime et du langage go

Traduction d'une partie des spécifications officielles du langage go, cet article explique comment go gère la mémoire.   Lire »

Comment gérer efficacement les erreurs en golang ?

Préconisations officielles pour la gestion des erreurs dans un programme golang. Cet article complète les explications sur panic, defer et recover   Lire »

Commentaires

Soyez le premier à commenter cet article

Publier un commentaires

Tous les commentaires sont soumis à modération. Les balises HTML sont pour la plupart autorisées. Les liens contextuels et intéressants sont en follow.

(requis)
(requis)
(requis, mais non publié)
(recommandé si vous souhaitez être publié)