Dessiner en C : le format PPM

L’un des problème parallèle les plus simple est le traitement d’image. Afin de permettre la mise en place de ce type d’exemple, il faut se donner les moyen de générer des images en C. C’est le but de cet article qui va définir une petite bibliothèque de dessin basée sur le format PPM.

Portable PixMap

PPM signifie Portable PixMap c’est un format d’image extrêmement portable vous pouvez en lire plus ICI. Nous l’avons retenu car il est extrêmement simple. Il permet de générer des images en quelques lignes de C!

Afin de générer ce format de fichier, il faut avant tout en lire les spécifications http://netpbm.sourceforge.net/doc/ppm.html. En consultant ce document on peut avoir une idée plus claire du format:

P6
LARGEUR(int)
HAUTEUR(int)
MAXCOULEUR(int)
RASTER DE PIXEL

Par simplicité nous avons retenu le retour à la ligne comme séparateur. Le raster de pixel, est tout simplement un tableau de LARGEUR * HAUTEUR éléments. Le type de l’élément, comme indiqué dans la norme est un struxture regroupant les trois composantes sur un octet pour un MAXCOULEUR inférieur ou égal à 256, et deux octets pour une valeur maximale de 65536. Nous allons couvir le cas où nous encodons sur un octet, très classiquement pour une image. Les trois composantes sont Rouge, Vert et Bleu, notées R, G et B dans le reste de cet article.

Formaliser l’interface

En lisant les spécifications nous avons identifié deux objets, des pixels et une image. Cette image aura un ensemble de pixels et les attributs largeur et hauteur.

Le pixel

On définit alors un pixel comme une structure C:

struct ppm_pixel
{
	unsigned char r;
	unsigned char g;
	unsigned char b;
};

On retrouve très simplement nos trois composantes. Nous ajoutons également une fonction inline pour remplir un pixel. Une fonction quand définie dans le header peut être « recopiée » dans les différents fichiers, devenant ainsi plus efficace. Il faut lui ajouter le mot-clef « static » pour ne pas créer de conflits de noms, cette fonction étant alors définie dans tous les fichiers où le header a été inclus. Pour le pixel définissons un setter:

static inline void ppm_setpixel( struct ppm_pixel * px, unsigned char r , unsigned char g , unsigned char b)
{
	px->r = r;
	px->g = g;
	px->b = b;
}

Cette fonction est extrêmement simple, on place simplement les paramètres dans les membres de la structure.

L’image

Ensuite définissons une image:

struct ppm_image
{
	unsigned int width;
	unsigned int height;
	struct ppm_pixel * px;
};

Simple également, une largeur (width), une hauteur (height) et des pixels. Maintenant définissons les fonctions qui seront le point d’entrée de l’utilisateur:

int ppm_image_init( struct ppm_image *im , int w , int h );
int ppm_image_release( struct ppm_image *im );
void ppm_image_setpixel( struct ppm_image * im, int x, int y, unsigned char r , unsigned char g , unsigned char b);
int ppm_image_dump( struct ppm_image *im, char * path );

Nous allons maintenant les couvrir une par une.

ppm_image_init

Cette fonction doit instancier une image PPM, en particulier il s’agit de procéder à l’allocation mémoire de cette image pour la rendre manipulable par les primitives de dessin. Elle a été implémentée comme suit:

int ppm_image_init( struct ppm_image *im , int w , int h )
{
	memset( im, 0, sizeof( struct ppm_image));

	im->width = w;
	im->height = h;

	im->px = calloc( sizeof(struct ppm_pixel), w*h );

	if( !im->px)
	{
		perror("malloc");
		return 1;
	}

	return 0;
}

On vide le contenu de la structure, puis on sauve largeur et hauteur dans la structure. Enfin on utilise la fonction calloc pour allouer de la mémoire constituant notre raster de pixel. Calloc étant une manière optimisée d’allouer de la mémoire à « 0 ». (pas de malloc + memset!). Et voilà la base de l’image est prête.

ppm_image_release

Ici on veut simplement libérer une image, tout en rendant la mémoire associée au système.

int ppm_image_release( struct ppm_image *im )
{
	if( im == NULL )
		return 1;

	free( im->px );
	im->px = NULL;

	im->width = 0;
	im->height = 0;

	return 0;
}

On prends le temps de tout mettre à zéro dans la structure. Ensuite on fait « free » de la mémoire. Il est important de mettre le pointeur à NULL. En effet free ne produit pas d’erreur si on lui passe un pointeur NULL. Ainsi vous couvrez le cas où une image est libérée (par erreur) deux fois sans crasher.

ppm_image_dump

Cette fonction va se charger d’écrire notre fichier au format PPM. Le processus est assez simple, il suffit de se reporter au standard.

int ppm_image_dump( struct ppm_image *im, char * path )
{
	FILE * out = fopen( path , "w");

	if( !out )
	{
		perror("fopen");
		return 1;
	}

	fprintf(out, "P6\n");
	fprintf(out, "%d\n", im->width);
	fprintf(out, "%d\n", im->height);
	fprintf(out, "255\n");
	fwrite( im->px, sizeof(struct ppm_pixel) , im->width * im->height, out );

	fclose( out );

	return 0;
}

Ici nous ouvrons tout d’abord le fichier « path » en écriture. Ensuite, nous écrivons le « magic number » de PPM, le P6. Puis les largeurs et hauteurs. Enfin après avoir donné la valeur maximum de 255 suivie d’un retour chariot, nous écrivons le raster de pixel directment en binaire. Et voilà, l’image est sauvegardée.

ppm_image_setpixel

Il faut bien sûr une fonction pour dessiner un pixel. Cette fonction étant un accesseur soumis à des contraintes de performances (de nombreux pixels modifiés par cet appel de base), nous le placerons dans le header en static inline. Le code est le suivant:

static inline void ppm_image_setpixel( struct ppm_image * im, int x, int y, unsigned char r , unsigned char g , unsigned char b)
{
	struct ppm_pixel * px = im->px + im->width * y + x;
	ppm_setpixel( px, r, g, b);
}

La seule complexité est le calcul de l’indice du pixel (tableau 2D) dans un tableau C issu d’une allocation linéaire. Le principe est de se déplacer selon Y en blocs de la largeur et ensuite d’indicer ce déplacement par la position en X. Ensuite, nous appelons le setter de pixel qui est également static inline.

Testons le tout

Il faut maintenant tester le tout en générant une image de test. Mais tout d’abord il faut compiler notre bibliothèque. Nous allons faire un makefile simple.

CC=gcc
CFLAGS=-O3 -g

libppm.so : ppm.c
	$(CC) $(CFLAGS)  -fpic -shared $^ -o $@

Définissons tout d’abord une cible pour notre bibliothèque PPM. Notez que toute commande après une cible doit être indendée avec une tabulation. Pour compiler on passe « -fpic » qui compile le code de manière position indépendante (indispensable pour faire une bibliothèque dynamique). Ensuite « -shared » pour construire un .so. Puis « $^ » renvoie aux dépendances après le « : » de la cible. Enfin, « $@ » est le nom de la cible, construisant finalement « libppm.so ».

Maintenant écrivons un petit fichier test (main.c):

#include "ppm.h"


int main(int argc, char *argv[])
{
	struct ppm_image im;

	ppm_image_init( &im, 1024, 1024 );

	int i,j;

	for (i = 0; i < 1024; ++i) {
		for (j = 0; j < 1024; ++j) {
			ppm_image_setpixel( &im, i, j, i%255, j%255, (i+j)%255);
		}
	}

	for (i = 0; i < 1024; ++i) {
		ppm_image_setpixel( &im, i, i, 255, 0, 0 );
	}

	ppm_image_dump( &im , "test.ppm");

	ppm_image_release( &im );


	return 0;
}

Ce code crée une image de 1024×1024, dessine des formes colorées et enfin une diagonale en rouge. Ce dernier test permet de vérifier la signification des X et Y que nous avons définis tout à l’heure. (0,0) étant en haut à gauche.

Ajoutons maintenant la cible dans le Makefile:

TARGET=test

all: $(TARGET)

test: main.c libppm.so
	$(CC) $(CFLAGS) $(LDFLAGS) -lppm -L. main.c -o $@

Ici nous définissons la cible « all » qui est celle appelée par défaut (quand on appelle make). Cette cible dépends de TARGET qui contient la cible « test ». All appelle donc test. Test dépends de main.c (elle est appelée si le fichier est modifié) et de libpp.so. Ansi libppm.so est construite pour construire test. Enfin, dans la ligne de commande, nous ajoutons « -lppm » (notez que c’est la fin du nom de libppm, par standard) et « -L. » pour dire à GCC de chercher les librairies dans le répertoire courant « . ».

Maintenant si nous faisons Make:

make
gcc -O3 -g  -fpic -shared ppm.c -o libppm.so
gcc -O3 -g  -lppm -L. main.c -o test

Ajoutons une cible « clean »:

clean:
	rm -fr $(TARGET) *.so

Et voilà le makefile est complet.

Maintenant si on lance le test, on a une erreur :

./test: error while loading shared libraries: libppm.so: cannot open shared object file: No such file or directory

Cela est dû au fait que nous n’avons pas informé le loader de la position du binaire. Il faut l’ajouter au LD_LIBRARY_PATH. On peut vérifier d’où viennent les bibliothèques avec « ldd »:

ldd ./test                                                          
	linux-vdso.so.1 =>  (0x00007ffdc674c000)
	libppm.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007f35861de000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f35865b9000)

Ajoutons le dossier courant à LD_LIBRARY_PATH:

export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH

Revérifions ldd:

ldd ./test
	linux-vdso.so.1 =>  (0x00007ffc0dd16000)
	libppm.so => /home/jbbesnard/ppm/libppm.so (0x00007f15a8d89000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f15a89af000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f15a8f8c000)

Et voilà, le programme peut s’éxécuter. Vous obtiendrez alors une image PPM dans le dossier courant. Vous avez maintenant une bibliothèque simple de dessin à votre disposition.

Conclusion

Nous avons vu comment implémenter une bibliothèque de dessin minimale supportant le format PPM. Cette bibliothèque servira de base à des exemples de traitement d’image par la suite.

Listings

Le code d’une bibliothèque PPM simpliste.

ppm.h:

#ifndef PPM_H
#define PPM_H

struct ppm_pixel
{
	unsigned char r;
	unsigned char g;
	unsigned char b;
};

static inline void ppm_setpixel( struct ppm_pixel * px, unsigned char r , unsigned char g , unsigned char b)
{
	px->r = r;
	px->g = g;
	px->b = b;
}

struct ppm_image
{
	unsigned int width;
	unsigned int height;
	struct ppm_pixel * px;
};

int ppm_image_init( struct ppm_image *im , int w , int h );
int ppm_image_release( struct ppm_image *im );

static inline void ppm_image_setpixel( struct ppm_image * im, int x, int y, unsigned char r , unsigned char g , unsigned char b)
{
	struct ppm_pixel * px = im->px + im->width * y + x;
	ppm_setpixel( px, r, g, b);
}

int ppm_image_dump( struct ppm_image *im, char * path );


#endif /* PPM_H */

ppm.c:

#include "ppm.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int ppm_image_init( struct ppm_image *im , int w , int h )
{
	memset( im, 0, sizeof( struct ppm_image));

	im->width = w;
	im->height = h;

	im->px = malloc( w * h * sizeof(struct ppm_pixel));

	if( !im->px)
	{
		perror("malloc");
		return 1;
	}

	return 0;
}

int ppm_image_release( struct ppm_image *im )
{
	if( im == NULL )
		return 1;

	free( im->px );
	im->px = NULL;

	im->width = 0;
	im->height = 0;

	return 0;
}

int ppm_image_dump( struct ppm_image *im, char * path )
{
	FILE * out = fopen( path , "w");

	if( !out )
	{
		perror("fopen");
		return 1;
	}

	fprintf(out, "P6\n");
	fprintf(out, "%d\n", im->width);
	fprintf(out, "%d\n", im->height);
	fprintf(out, "255\n");
	fwrite( im->px, sizeof(struct ppm_pixel) , im->width * im->height, out );

	fclose( out );

	return 0;
}

Makefile:

CC=gcc
CFLAGS=-O3 -g

TARGET=test

all: $(TARGET)

libppm.so : ppm.c
	$(CC) $(CFLAGS)  -fpic -shared $^ -o $@

test: main.c libppm.so
	$(CC) $(CFLAGS) $(LDFLAGS) -lppm -L. main.c -o $@

clean:
	rm -fr $(TARGET) *.so