Couleurs, images et Xlib

Pierre Ficheux (pficheux@com1.fr)

Novembre 1999


0. Résumé

Cet article présente de manière simplifiée la gestion des couleurs et des images sous X Window. Bien qu'étant illustré pas des exemples utilisant la bibliothèque de base Xlib, les concepts décrits sont applicables à tous les niveaux de la programmation X. Nous utiliserons la terminologie décrite dans l'article d'introduction à la Xlib paru dans LM numéro 10.

Les exemples des programmation sont disponibles en téléchargement sur http://www.com1.fr/~pficheux/articles/lmf/xlib-images/xlib-images_exemples.tar.gz

1. Le principe du codage des couleurs sous X

En Xlib, la méthode standard de codage des couleurs est similaire à celle utilisée sur les tubes télévision ou moniteur: chaque élément d'image est constitué d'un mélange des trois couleurs de base (rouge/vert/bleu). La proportion de chaque composante détermine la couleur. On parle dans ce cas de codage RGB (Red Green Blue).

Dans le cas de la Xlib, la valeur de chaque composante est codée sur un octet soit de 0 à 255. On a par exemple:

2. Les limitation suivant le type de DISPLAY

D'après le paragraphe précédent, chaque pixel est constitué de trois octets. Cette configuration est très satisfaisante car elle permet d'obtenir 16 millions de couleurs (256 ^ 3). Par contre, la mémoire d'image nécessaire est très important, surtout si l'on se resitue à l'époque de la conception de X Window (début/milieu des années 1980).

Les contraintes matérielles ont donc conduit les concepteurs de X à définir différents type des codage plus restrictifs mais aussi moins consommateurs de mémoire vidéo.

Le DISPLAY X est caractérisé entre-autres par le nombre de bits utilisés pour coder un pixel. Dans le cas précédent, chaque pixel occupe 3 octets soit 24 bits. On parlera d'un DISPLAY à 24 plans. On peut connaitre le nombre de plans du DISPLAY courant en utilisant la macro Xlib DisplayPlanes(display). Pour simplifier le problème, on distinguera 3 types de DISPLAY:

Au début des années 1990, il existait également de nombreux DISPLAY 1 plan, capables seulement d'afficher des pixels noirs ou blancs.

Le nombre de couleurs décrit ci-dessus correspond au nombre de couleurs affichables simultanément sur l'écran. Nous verrons plus loin qu'il est possible d'avoir une palette de 16M de couleurs même si l'on ne peut en afficher que 256 à la fois.

Les caractéristiques d'affichage de votre DISPLAY sont accessible à l'aide de la commande xdpyinfo qui pourra entre-autres donner le type d'information suivante:

screen #0:
  dimensions:    1152x900 pixels (390x305 millimeters)
  resolution:    75x75 dots per inch
  depths (1):    16
  root window id:    0x25
  depth of root window:    16 planes

3. Description des 3 types de DISPLAY

3.1 Le DISPLAY à 8 plans

Dans ce cas la, chaque pixel est codé sur un seul octet. On ne peut donc afficher simultanément que 256 couleurs sur une même table de couleurs (ou Colormap). La Colormap par défaut est donnée par la macro Xlib DefaultColormap(screen) et l'on peut bien entendu créer de nouvelles Colormap au cours de l'exécution d'un programme. A un instant donné, chaque Colormap dispose de 256 couleurs différentes puisées dans la palette de 16M de couleurs.

Ce type de codage dit PseudoColor est encore largement utilisé aujourd'hui dans le cas de cartes vidéos disposant de faible capacité mémoire (1 Mo). Il est cependant très contraignant pour plusieurs raisons:

3.2 Le DISPLAY à 16 plans

Dans ce type de codage, il n'y a pas de table de couleurs (on parle de codage réel ou TrueColor). Chaque pixel est codé sur 16 bits ce qui oblige à réduire le nombre de bits significatifs pour chaque couleur R, V ou B.

Dans le schéma ci-dessus les composantes R/V/B utilisent respectivement 5, 6 et 5 bits. Cette information est disponible par la commande xpdyinfo au niveau des paramètres de masques:

red, green, blue masks:    0xf800, 0x7e0, 0x1f

On dispose donc de 65536 couleurs (2 ^16).

3.3 Le DISPLAY à 24 plans

Ce DISPLAY n'est qu'une extension du cas précédent dans lequel chaque composante utilise 8 bits. On dispose donc de 16M de couleurs. Ce codage était autrefois réservé aux stations graphiques haut de gamme (SGI en particulier) mais il s'est aujourd'hui démocratisé grace aux performances toujours accrues des cartes graphiques PCI et AGP. N'oublions pas cependant qu'un codage sur 24 bits nécessitera plus de puissance de calcul, en particulier lors de la recopie de portions d'écran (plus d'octets à manipuler).

Dans le cas général d'une utilisation classique (pas de traitement d'image), un DISPLAY 16 bits est un bon compromis.

3.4 Test des différents types de codage

Si votre serveur X est bien configuré, il est relativement simple de tester les 3 types de codage, par exemple en utilisant simplement la commande xinit. Par défaut, xinit lancera le serveur X en mode 8 plans. Si vous voulez utiliser un autre mode, il faudra passer au serveur l'option adéquate (-bpp) :
   xinit -- -bpp 16

4. Manipulation des couleurs en Xlib

4.1 Le principe

Le principe de manipulation des couleurs en Xlib est le suivant:

Dans le cas de DISPLAY TrueColor, vous pouvez également calculer directement la valeur du pixel à partir du RVB et des caractéristiques du DISPLAY. Ceci permet de ne pas effectuer d'accès au serveur X (pas d'appel à XAllocColor) et donc d'optimiser le temps de traitement (voir l'exemple de manipulation d'image au paragraphe 5.2).

L'allocation de couleur sera effectuée par la fonction:

   XAllocColor (Display *display, Colormap cmap, XColor *color)

Le type XColor permet de spécifier les composantes RGB de la couleur à allouer. En cas de succès, la variable color contiendra la valeur du pixel.

typedef struct {
	unsigned long pixel;
	unsigned short red, green, blue;
	char flags;  /* do_red, do_green, do_blue */
	char pad;
} XColor;

Il existe un certain nombre de couleurs définies par des noms et listées dans le fichier /usr/X11R6/lib/X11/rgb.txt:

211 211 211		light grey
211 211 211		LightGrey
211 211 211		light gray
211 211 211		LightGray
 25  25 112		midnight blue
 25  25 112		MidnightBlue
  0   0 128		navy
  0   0 128		navy blue
  0   0 128		NavyBlue
Ce fichier permet de manipuler plus facilement les noms de couleurs courantes en utilisant la fonction:
  XAllocNamedColor (Display *display, Colormap cmap, char *color_name, 
    XColor *color, XColor *exact)

Le dernier paramètre retournera la valeur exacte de la couleur spécifiée par color_name alors que color retournera la valeur approchée supportée par le screen courant.

La valeur du pixel est ensuite utilisable dans un GC (contexte graphique) utilisé pour tous les types de tracés (voir article Xlib LM10), par exemple:

  XSetForeground (display, gc, color.pixel)

4.2 Un petit exemple

L'exemple ci-dessous affiche des carrés multicolores de 10x10 dans une fenêtre 320x250. La structure général est très proche de celle de l'article Xlib de LM10.

On définit un tableau contenant 8 noms de couleurs et un tableau de 8 variables de type XColor:

static char *color_names[] = {
    "black",
    "red",
    "green",
    "yellow",
    "blue",
    "magenta",
    "cyan",
    "white"
  };

static XColor x_colors[8];
On alloue les couleurs définies par leurs noms sur la Colormap par défaut:
    cmap = DefaultColormap(display, screen);

    /* Allocation des couleurs  */
    for (i = 0; i != 8; i++) {
      if (!XAllocNamedColor (display, cmap, color_names[i], &x_colors[i], &exact)) {
	fprintf(stderr, "cant't alloc color %s\n", color_names[i]);
	exit (1);
      }
    }
Lors de la réception de l'évènement Expose, on affiche la fenêtre en la remplissant de carrés multicolores:
void expose ()
{
    int x, y, color = 0;

    for (x = 0 ; x < WIDTH ; x += 10) {
	for (y = 0 ; y < HEIGHT ; y += 10) {
	  XSetForeground (display, gc, x_colors[color].pixel);
	  XFillRectangle (display, win, gc, x, y, 10, 10);
	  color = (++color > 7 ? 0 : color);
	}
    }
}
Le résultat à l'affichage est le suivant:

5. Traitement des images

5.1 Principe et fonctions à utiliser

Nous avons vu dans l'article Xlib de LM10 que nous pouvions effectuer des tracés dans un type de donnée générique appelé Drawable. Le Drawable est soit une Window (fenêtre) ou bien un Pixmap (qui est en quelque sorte une fenêtre invisible). Le Drawable est localisé dans la mémoire du serveur X qui n'est pas forcément celle du calculateur qui exécute l'application (n'oublions pas que X est un système graphique réparti).

Pour manipuler les images, nous allons utiliser un type particulier appelé XImage. Un des champs du type XImage sera un pointeur vers les données réelles de l'image (la définition des pixels). La création d'une image sera réalisée par la fonction:

       XImage *XCreateImage(display, visual, depth, format, off-
       set, data, width, height, bitmap_pad,
                               bytes_per_line)
             Display *display;
             Visual *visual;
             unsigned int depth;
             int format;
             int offset;
             char *data;
             unsigned int width;
             unsigned int height;
             int bitmap_pad;
             int bytes_per_line;

La zone mémoire pointée par data devra contenir l'image codée au format du DISPLAY courant (8, 16 ou 24 bits). Le remplissage de cette mémoire sera effectué par la fonction:
       XPutPixel(ximage, x, y, pixel)
             XImage *ximage;
             int x;
             int y;
             unsigned long pixel;
Vous remarquerez que cette fonction n'utilise pas de paramètre display pour la bonne raison que la zone de donnée de l'XImage est définie dans la mémoire du calculateur et non dans celle du serveur X.

Lorsque l'on voudra afficher l'image dans un Drawable, on devra utiliser la fonction:

       XPutImage(display, d, gc, image, src_x, src_y, dest_x,
       dest_y, width, height)
               Display *display;
               Drawable d;
               GC gc;
               XImage *image;
               int src_x, src_y;
               int dest_x, dest_y;
               unsigned int width, height;

5.2 Un exemple complet commenté

L'exemple ci-dessous vous permettra d'afficher des fichier ppm sur des DISPLAY de type 8, 16 ou 24 bits. Le format ppm est largement utilisé comme format intermédiaire par des packages de conversion graphique comme NetPbm. Son format est simple
En-tête (magic number = 'P6')
largeur_image hauteur_image
255
description du pixel 1 en RVB
description du pixel 2 en RVB
etc...
Par exemple:
P6
359 352
255
U&(54"!º}'·
...
Le principe du programe est le suivant:
  1. calcul des paramètres du DISPLAY suivant le type de codage
  2. dans le cas d'un DISPLAY 8 bits, on crée une nouvelle Colormap
  3. on alloue une XImage
  4. on lit le fichier ppm, on alloue les différentes couleurs nécessaires et on remplit la zone data de l'XImage avec les pixels calculés en fonction du type de DISPLAY
  5. lorque l'image est prête, on l'affiche dans une fenêtre par l'évènement Expose

Dans le cas d'un DISPLAY 8 bits, on limitera le nombre d'appels à XAllocColor (très gourmant en ressources X) en gèrant un cache très simple qui stockera les valeurs RVB allouées au fur et à mesure de la lecture du fichier. Dans le cas des autre configurations, on optimisera le code en calculant directement la valeur du pixel.

On commence par déterminer la Colormap par défaut et le nombre de plans:

    default_cmap = DefaultColormap (display, screen);
    nplanes = DisplayPlanes (display, screen);

Si l'on utilise un codage TrueColor on doit calculer le nombre de bits associés à chaque composante ainsi que la position de la composante dans la définition du pixel:

    if (nplanes > 8) {
      /* On utilise dans ce cas la colormap par défaut */
      current_cmap = default_cmap;

      /* Calcul des decalages */
      r = red_mask = the_visual->red_mask;
      g = green_mask = the_visual->green_mask;
      b = blue_mask = the_visual->blue_mask;
	
      red_bits = 0;
      green_bits = 0;
      blue_bits = 0;
      red_shift = 0;
      green_shift = 0;
      blue_shift = 0;
    
      while (!(r & 1)) {
	r >>= 1;
	red_shift++;
      }
      while (r & 1) {
	r >>= 1;
	red_bits++;
      }
      ...
    }

Si on est en 8 bits, on alloue une nouvelle Colormap en dupliquant celle par défaut:

    else
      current_cmap = XCopyColormapAndFree (display, default_cmap);

On effectue ensuite la lecture du fichier ppm et on alloue la mémoire nécessaire au stockage de l'image:

    fscanf (fp, "%c%c\n", &magic[0], &magic[1]);

    if (magic[1] != '6') {
      fprintf (stderr, "%s: %s is not a PPM file.\n", av[0], av[1]);
      fclose (fp);
      exit (1);
    }

    fscanf (fp, "%d %d\n", &image_width, &image_height);
    fscanf (fp, "%d\n", &nb_colors);

    if (!(image_data = (char*) calloc (1, sizeof(unsigned int) * image_width * image_height))) {
	perror ("calloc image_data:");
	exit (1);
    }
On alloue ensuite l'XImage. Notez la compilation conditionnelle suivant le type de processeur little endian (x86 ou équivalents) ou big endian (SPARC, 68k etc...).
    ximage = XCreateImage (display, DefaultVisual (display, screen), nplanes, ZPixmap, 
             0, image_data, image_width, image_height, 8 * sizeof(unsigned int), 0) ;
#ifdef __i386__		
    ximage->byte_order = LSBFirst;
#else
    ximage->byte_order = MSBFirst;
#endif
On lit ensuite les triplés RVB à partir du fichier. Dans le cas d'un codage TrueColor, on calcule la valeur du pixel directement à partir des composantes, des masques et des décalages (pas d'allocation).
    for (y = 0 ; y < image_height ; y++) {
      for (x = 0 ; x < image_width ; x++) {

	fscanf (fp, "%c%c%c", &red, &green, &blue);

	if (nplanes > 8) {
	  red >>= (8 - red_bits);
	  green >>= (8 - green_bits);
	  blue >>= (8 - blue_bits);

	  pixel = ((red << red_shift) & red_mask) | ((green << green_shift) & green_mask) 
                  | ((blue << blue_shift) & blue_mask);

	}
Dans le cas du 8 bits, on doit tout d'abord vérifier que la couleur n'est pas déja dans le cache:
	/* 8 planes */
	else {
	  XColor color;

	  red <<= 8;
	  green <<= 8;
	  blue <<= 8;

	  for (i = 0 ; i != last_color ; i++) {
	    if (color_cache[i].red == red && color_cache[i].green == green 
               && color_cache[i].blue == blue)
	      break;
	  }
Si elle n'y est pas, on doit allouer la couleur et la stocker dans le cache. Si on dépasse le nombre de couleurs dans la Colormap (256) on sort...
	  if (i == last_color) {
	    color.red = red;
	    color.green = green;
	    color.blue = blue;

	    if (!XAllocColor (display, current_cmap, &color)) {
	      fprintf (stderr, "Can't allocate %d %d %d\n", red, green, blue);
	      exit (1);
	    }
	    else {
	      color_cache[i].red = red;
	      color_cache[i].green = green;
	      color_cache[i].blue = blue;
	      color_cache[i].pixel = pixel = color.pixel;
	    }

	    last_color++;
	    if (last_color > 256)  {
	      fprintf (stderr, "Too many colors...\n");
	      exit (1);
	    }
	  }
Si elle y est, il suffit de récupérer la valeur du pixel:
	  else
	    pixel = color_cache[i].pixel;
Dans tous les cas, on écrit le pixel calculé dans l'image:
	XPutPixel (ximage, x, y, pixel);
Lorsque l'image est prête, on crée la fenêtre d'affichage, puis on affiche l'image dans l'évènement Expose:
    XPutImage (display, win, gc, ximage, 0, 0, 0, 0, image_width, image_height);
On pourrait améliorer ce programme en lui ajoutant une fonction de réduction de couleurs (dithering) dans le cas du mode 8 bits. Un algorithme de dithering est disponible dans le package NetPbm.

6. Conclusion

La manipulation des couleurs et images sous X n'est pas forcément très simple. Il est bien entendu possible de s'affranchir le plus souvent de ces manipulations en utilisant des bibliothèques de plus haut niveau que la Xlib mais la compréhension et les performances n'ont pas de prix ;-))

7. Bibliographie


© Copyright 2000 Diamond Editions/Linux magazine France
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; A copy of the license is included in the section entitled "GNU Free Documentation License".