Soutenez-nous

Charger des images PNG

Cet article a pour objectif d'expliquer comment charger une image PNG afin de créer une texture OpenGL. Nous utiliserons pour cela la bibliothèque libpng. Les exemples donnés dans ce document sont écrits en C.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Google Bookmarks ! Facebook Digg del.icio.us Yahoo MyWeb Blinklist Netvouz Reddit Simpy StumbleUpon Bookmarks Share on Google+ 

1. Introduction

Cet article a pour objectif d'expliquer comment charger une image PNG afin de créer une texture OpenGL. Nous utiliserons pour cela la bibliothèque libpng. Les exemples donnés dans ce document sont écrits en C.

« PNG » signifie « Portable network graphics ». C'est un format de fichier qui a été créé dans l'intention de remplacer le format GIF(1). Il possède les caractéristiques suivantes :

  • Capable de stocker des images avec palette couleurs (8 bits), en dégradé de gris (8 et 16 bits) et en vraies couleurs (24 et 32 bits) ;
  • Canal alpha (256 niveaux de transparence pour les images 32 bits, un pour les images 8 bits) ;
  • Utilise un algorithme de compression sans perte ;

La bibliothèque libpng est en quelque sorte la bibliothèque officielle pour manipuler les fichiers PNG. Elle a été écrite en accompagnement aux spécifications du fichier, disponible sur http://www.libpng.org/pub/png/. Cette bibliothèque utilise zlib pour la compression et décompression des images. Elle est Open Source.

La libpng propose un ensemble de fonctions pour manipuler facilement les images PNG et de plusieurs manières possibles. Cet article ne se veut pas être une documentation exhaustive de l'utilisation de libpng. Pour plus d'informations, reportez vous à son manuel en ligne.

Nous allons voir ici deux méthode pour lire les images PNG. La première, en lisant les données depuis un fichier, et la seconde en chargeant l'image depuis une zone mémoire où les données seraient stockées.

2. Texture OpenGL

Avant de commencer à lire le fichier, il est nécessaire de connaître les données dont nous avons besoin pour créer une texture OpenGL.

Il nous faut bien évidemment un tableau de données pour stocker les pixels de l'image. OpenGL peut prendre plusieurs types de données pour créer une texture (octets, entiers, etc.). Nous nous restreindront ici au type « octet non signé » pour stocker nos données.

Nous avons également besoin de connaître les dimensions de l'image, le nombre de composantes R, V, B, A ou Luminance et Alpha de l'image et le format (RVB, RVBA, Luminance ou Luminance Alpha).

Nous allons regrouper ces données dans une type structure gl_texture_t :

 
Sélectionnez
/* OpenGL texture info */
typedef struct
{
    GLuint  width;           /* largeur */
    GLuint  height;          /* hauteur */
 
    GLenum  format;          /* RVB, RVBA, Luminance, Luminance Alpha */
    GLint   internalFormat;  /* composantes d'un texel */
 
    GLubyte *texels;         /* données de l'image */
 
} gl_texture_t;

Pour la lecture d'une image TGA, nous créeront une fonction qui prendra en paramètre un nom de fichier et un pointeur vers un objet de type gl_texture_t. Une fois l'image chargée, nous pourront utiliser les fonctions OpenGL pour créer une texture à partir de notre objet gl_texture_t.

3. Structures de la libpng

Les deux principales structures de la libpng sont png_struct et png_info. png_struct représente un objet « image png » et est utilisée en interne par la bibliothèque. On la passe à quasiment toutes les fonctions de libpng. png_info quant à elle, permet à l'utilisateur de récupérer des informations sur l'image, comme ses dimensions, sa profondeur, sa palette, etc.

Toutes les fonctions de la bibliothèque libpng commencent par « png_ ».

Le fichier d'en-tête à inclure pour utiliser la libpng est png.h. Ce fichier inclut les fichiers pngconf.h et zlib.h. Ce dernier inclut zconf.h. Si vous comptez redistribuer la bibliothèque avec votre code source, ne les oubliez pas.

Pour utiliser la libpng, assurez vous donc d'inclure png.h :

 
Sélectionnez
#include <png.h>

Vous devrez également compiler votre programme avec libpng et zlib. En ligne de commande (ou dans un Makefile), utilisez la commande libpng12-config --cflags pour obtenir les paramètres de compilation nécessaires à libpng et libpng12-config --libs pour l'inclusion des bibliothèques à la liaison. Exemple avec un programme constitué de deux fichiers source, main.c et png.c :

 
Sélectionnez
gcc -c main.c -o main.o
gcc `libpng12-config --cflags` -c png.c -o png.o
gcc `libpng12-config --libs` -lz main.o png.o -o monprogramme

4. Lecture d'une image PNG depuis un fichier

Nous allons écrire une fonction ReadPNGFromFile() prenant en paramètre un nom de fichier et renvoyant une structure gl_texure_t comme définie plus haut. Voici le prototype de la fonction :

 
Sélectionnez
gl_texture_t *ReadPNGFromFile (const char *filename);

4.1. Ouverture du fichier et vérification

Tout d'abord, nous devons ouvrir le fichier :

 
Sélectionnez
FILE *fp;
png_byte magic[8];
 
/* open image file */
fp = fopen (filename, "rb");
if (!fp)
  {
    fprintf (stderr, "error: couldn't open \"%s\"!\n", filename);
    return NULL;
  }
 
/* read magic number */
fread (magic, 1, sizeof (magic), fp);
 
/* check for valid magic number */
if (!png_check_sig (magic, sizeof (magic)))
  {
    fprintf (stderr, "error: \"%s\" is not a valid PNG image!\n",
             filename);
    fclose (fp);
    return NULL;
  }

Les huit premiers octets d'un fichier PNG contiennent un numéro magique servant à authentifier qu'il s'agit d'un fichier PNG. Si ces huits octets ne correspondent pas à la signature d'un fichier PNG, inutile de continuer. La fonction png_check_sig() de libpng permet de tester si une chaîne contient bien la signature d'un fichier PNG.

4.2. Création des structures « png_struct » et « png_info »

Maintenant que l'on s'est assuré qu'il s'agit bien d'un fichier PNG, on peut créer nos deux structures png_struct et png_info. Les fonctions png_create_read_struct () et png_create_info_struct () se chargent de ça :

 
Sélectionnez
/* create a png read struct */
png_structp png_ptr = png_create_read_struct
  (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr)
  {
    fclose (fp);
    return NULL;
  }
 
/* create a png info struct */
png_infop info_ptr = png_create_info_struct (png_ptr);
if (!info_ptr)
  {
    fclose (fp);
    png_destroy_read_struct (&png_ptr, NULL, NULL);
    return NULL;
  }

Tout d'abord, vous devez remarquer que l'on utilise le type png_structp au lieu de png_struct comme je l'ai dis plus haut. En fait, png_structp est définie comme étant un pointeur sur un png_struct. png_ptr est donc un pointeur sur un png_struct. Il en est de même pour info_ptr qui est un pointeur sur un png_info.

La fonction png_create_read_struct () prend quatre paramètres. Le premier est la version de la bibliothèque. La macro PNG_LIBPNG_VER_STRING permet de passer la version de la bibliothèque avec laquelle on compile le programme. Les trois autres paramètres (optionnels) servent à définir les fonctions à utiliser pour la gestion des erreurs ; nous les verrons plus tard, en étudiant la seconde méthode de lecture d'une image PNG. Pour le moment, on peut les laisser à NULL.

La fonction png_create_info_struct () ne nécessite que notre objet png_struct en paramètre. En cas d'échec, on détruit nos structures avec png_destroy_read_struct (). Cette fonction prend en premier paramètre un png_struct (impérativement non nul) et en second un png_info. Le dernier paramètre est aussi une structure png_info que nous n'utiliseront pas ici.

Il est également temps de créer un objet de type gl_texture_t pour stocker les informations et données nécessaires à la création de notre texture :

 
Sélectionnez
/* create our OpenGL texture object */
gl_texture_t *texinfo = (gl_texture_t *)malloc (sizeof (gl_texture_t));

4.3. Gestion des erreurs de libpng

libpng utilise le mécanisme de setjmp/longjmp pour la gestion des erreurs. setjmp() et longjmp() sont des fonctions système qui proposent un mécanisme similaire aux exceptions en C++. setjmp() permet de sauvegarder le contexte de la pile à un endroit donné. longjmp() permet de faire un saut vers un contexte de pile sauvegardé par setjmp().

La fonction setjmp() renvoie 0 lorsqu'elle revient directement et une valeur non nulle lorsqu'elle revient à travers un appel à longjmp().

La structure png_struct possède une variable de type jmp_buf permettant de mémoriser un contexte (la destination d'un saut via longjmp()) pour y revenir en cas d'erreur lors de la manipulation des images. On y accède à l'aide de la fonction png_jmpbuf().

Il faut placer cette destination de saut avant le début du traitement de l'image et gérer l'interruption de la lecture en cas d'erreur.  :

 
Sélectionnez
/* initialize the setjmp for returning properly after a libpng
   error occured */
if (setjmp (png_jmpbuf (png_ptr)))
  {
    fclose (fp);
    png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
 
    if (texinfo)
      {
        if (texinfo->texels)
          free (texinfo->texels);
 
        free (texinfo);
      }
 
    return NULL;
  }

Lorsque libpng rencontre une erreur, il exécute un saut via longjmp() à cet endroit et se retrouve donc à l'intérieur du bloc if. On peut ainsi détruire les structures proprement et désallouer la mémoire utilisée pour la texture.

Remarque : si vous omettez de placer le setjmp(), libpng appellera alors abord() qui procèdera à la terminaison de votre programme. Ce n'est probablement pas un comportement souhaité pour votre application.

4.4. Initialisation de la source de lecture du fichier

libpng doit connaître la source de lecture du fichier. Il est possible de gérer soit même la lecture des données, depuis une source quelconque. libpng propose par défaut de lire l'image depuis un fichier en utilisant la fonction standard fread(). Pour cela, il faut lui spécifier le pointeur de fichier avant de commencer la lecture :

 
Sélectionnez
/* setup libpng for using standard C fread() function
   with our FILE pointer */
png_init_io (png_ptr, fp);
 
/* tell libpng that we have already read the magic number */
png_set_sig_bytes (png_ptr, sizeof (magic));

La fonction png_init_io() permet de passer notre pointeur de fichier à la structure png_struct. On spécifie ensuite à libpng le nombre d'octets déjà lus dans le fichier à l'aide de la fonction png_set_sig_bytes (). Rappelez vous, on a déjà lu huit octets pour la signature du fichier. libpng sait ainsi qu'il ne faut pas relire la signature.

Il est à présent possible de commencer à lire le fichier. Commençons par les informations stockées dans notre png_info :

 
Sélectionnez
/* read png info */
png_read_info (png_ptr, info_ptr);

4.5. Transformation de l'image source

Le format PNG peut contenir des images avec palette couleurs, en dégradé de gris et en couleurs vraies. On utilisera les formats GL_RGB et GL_RGBA pour les textures en couleurs vraies et GL_LUMINANCE et GL_LUMINANCE_ALPHA pour les images en dégradé de couleurs. On peut spécifier à libpng un certain nombre de transformations de l'image source avant de commencer la lecture. Récupérons d'abord le nombre de bits par pixel et le type d'image stockée dans le fichier PNG (PNG_COLOR_TYPE_GRAY, PNG_COLOR_TYPE_RGB, etc.) :

 
Sélectionnez
int bit_depth, color_type;
 
/* get some usefull information from header */
bit_depth = png_get_bit_depth (png_ptr, info_ptr);
color_type = png_get_color_type (png_ptr, info_ptr);

Il est nécessaire de transformer les images en palette couleurs en images couleurs vraies :

 
Sélectionnez
/* convert index color images to RGB images */
if (color_type == PNG_COLOR_TYPE_PALETTE)
  png_set_palette_to_rgb (png_ptr);

Certaines images en dégradé de gris peuvent être codées sur 1, 2 ou 4 bits. Il faut les étendre à du 8 bits pour pouvoir ensuite les utiliser avec OpenGL avec le format GL_LUMINANCE :

 
Sélectionnez
/* convert 1-2-4 bits grayscale images to 8 bits
   grayscale. */
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
  png_set_gray_1_2_4_to_8 (png_ptr);
 
if (png_get_valid (png_ptr, info_ptr, PNG_INFO_tRNS))
  png_set_tRNS_to_alpha (png_ptr);

Certaines images peuvent stocker un canal de couleur sur 16 bits qu'il faut alors ramener à 8 bits. De même, certaines images en 1, 2 ou 4 bits peuvent coder plusieurs canaux sur un octet (afin de gagner en mémoire utilisée) qu'il faut étendre à un octet par canal. libpng permet d'effectuer ces opérations sans perdre d'information sur les pixels :

 
Sélectionnez
if (bit_depth == 16)
  png_set_strip_16 (png_ptr);
else if (bit_depth < 8)
  png_set_packing (png_ptr);

Les images avec palette sont ainsi converties en images en couleurs vraies ou en images avec dégradé de gris sans palette. Chaque canal de couleur (rouge, vert, bleu, luminance, alpha) sera codé sur 8 bits. Il reste à valider ces transformations avec la fonction png_read_update_info() :

 
Sélectionnez
/* update info structure to apply transformations */
png_read_update_info (png_ptr, info_ptr);

Ces transformations seront appliquées lors de la lecture de l'image depuis le fichier. L'image source (stockée dans le fichier) ne sera pas modifiée. Mais l'image résultante (que l'on aura en mémoire) ne sera donc pas forcément exactement la même.

4.6. Récupération des informations sur l'image

Après avoir spécifié les transformations voulues, nous pouvons récupérer les informations sur l'image résultante afin d'initialiser les variables de notre structure gl_texture_t. La fonction png_get_IHDR() nous permet de récuperer directement les dimensions de l'image, le nombre de bits par pixels et le type d'image PNG :

 
Sélectionnez
/* retrieve updated information */
png_get_IHDR (png_ptr, info_ptr,
              (png_uint_32*)(&texinfo->width),
              (png_uint_32*)(&texinfo->height),
              &bit_depth, &color_type,
              NULL, NULL, NULL);

Il ne nous reste plus que le format et le nombre d'octets par pixels (le « internal format » nécessaire à glTexImage2D()) à retrouver, que l'on peut facilement déduire à partir du type d'image PNG :

 
Sélectionnez
switch (color_type)
  {
  case PNG_COLOR_TYPE_GRAY:
    texinfo->format = GL_LUMINANCE;
    texinfo->internalFormat = 1;
    break;
 
  case PNG_COLOR_TYPE_GRAY_ALPHA:
    texinfo->format = GL_LUMINANCE_ALPHA;
    texinfo->internalFormat = 2;
    break;
 
  case PNG_COLOR_TYPE_RGB:
    texinfo->format = GL_RGB;
    texinfo->internalFormat = 3;
    break;
 
  case PNG_COLOR_TYPE_RGB_ALPHA:
    texinfo->format = GL_RGBA;
    texinfo->internalFormat = 4;
    break;
 
  default:
    /* Badness */
    break;
  }

4.7. Lecture des données pixels

Nous sommes à présent pret pour lire les données pixels de l'image. Commençons par allouer de la mémoire pour les stocker dans notre gl_texture_t :

 
Sélectionnez
/* we can now allocate memory for storing pixel data */
texinfo->texels = (GLubyte *)malloc (sizeof (GLubyte) * texinfo->width
             * texinfo->height * texinfo->internalFormat);

La lecture de l'image se fait à l'aide de la fonction png_read_image(). Cette fonction attend en paramètre notre objet png_struct et un tableau de pointeurs pointant sur chacune des lignes de pixels de l'image.

On accède simplement au début d'une ligne de pixels de l'image avec l'instruction suivante :

 
Sélectionnez
row_pointers[i] = texinfo->texels[i * texinfo->width * texinfo->internalFormat];

Cependant le format PNG n'utilise pas le même système de coordonnées 2D. Le premier pixel d'une image PNG est situé dans le coin haut-gauche, tandis qu'OpenGL situe le premier pixel dans le coin bas-gauche. Il faut donc inverser verticalement l'image. Ceci peut se faire pendant la lecture de l'image en modifiant un peu notre tableau de pointeurs :

 
Sélectionnez
row_pointers[i] = texinfo->texels[(texinfo->height - (i + 1))
          * texinfo->width * texinfo->internalFormat];

La lecture des pixels donne donc ceci :

 
Sélectionnez
png_bytep *row_pointers;
 
/* setup a pointer array.  Each one points at the begening of a row. */
row_pointers = (png_bytep *)malloc (sizeof (png_bytep) * texinfo->height);
 
for (i = 0; i < texinfo->height; ++i)
  {
    row_pointers[i] = (png_bytep)(texinfo->texels +
      ((texinfo->height - (i + 1)) * texinfo->width * texinfo->internalFormat));
  }
 
/* read pixel data using row pointers */
png_read_image (png_ptr, row_pointers);
 
/* we don't need row pointers anymore */
free (row_pointers);

row_pointers est de type png_bytep, qui est un pointeur sur un octet. row_pointers est donc bien un tableau de pointeurs. Il faut aussi penser à le libérer dans le bloc if de setjmp().

png_read_image() va ainsi remplir notre tableau texinfo->texels via le tableau de pointeurs.

4.8. Fin de la lecture

Nous avons lu et stockée notre image dans un format qui nous permettra ensuite de créer une texture OpenGL. Il faut terminer la lecture de l'image et détruire proprement les structures allouées pour le décodage de l'image :

 
Sélectionnez
/* finish decompression and release memory */
png_read_end (png_ptr, NULL);
png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
 
fclose (fp);

On peut maintenant utiliser la structure gl_texture_t pour créer une texture OpenGL. Vous pouvez récupérer le code source complet de cette méthode de lecture depuis un fichier : png.c (9,0 Ko). Nous allons maintenant une autre méthode de lecture : depuis une zone mémoire où l'intégralité du fichier PNG est stockée.

5. Lecture d'une image PNG depuis une zone mémoire

Il vous arrivera peut-être d'avoir à traiter des fichiers qui sont déjà stockés en mémoire et donc sans utiliser les fonctions d'entrée/sortie standards (fopen(), fread(), fclose(), etc.).

De plus, vous pouvez préferer cette méthode pour lire des images depuis des fichiers sur le disque. Il est en effet plus rapide de lire la totalité d'un fichier dans un bloc mémoire que de faire beaucoup d'appels d'entrée/sortie pour lire un fichier morceau par morceau.

5.1. Buffer fichier

Commençons par définir une structure qui contiendra les données d'un fichier en mémoire :

 
Sélectionnez
/* file buffer struct */
typedef struct
{
  char name[256];
  unsigned char *data;
  long length;
  long offset;
 
} file_buffer_t;

Cette structure contient le nom du fichier (name), ses données (data), la taille du fichier en octets (length) et une variable offset indicant la position actuelle dans le fichier (utile pour la lecture).

Voyons rapidement la lecture d'un fichier dans une structure file_buffer_t :

 
Sélectionnez
file_buffer_t *
readFile (const char *filename)
{
  file_buffer_t *file;
  FILE *fp;
 
  /* open file */
  fp = fopen (filename, "rb");
  if (!fp)
    {
      fprintf (stderr, "error: couldn't open \"%s\"!\n", filename);
      return NULL;
    }
 
  /* create a file buffer */
  file = (file_buffer_t *)malloc (sizeof (file_buffer_t));
  if (!file)
    {
      fprintf (stderr, "Error: memory allocation failed "
	       "for \"%s\"\n", filename);
      fclose (fp);
      return NULL;
    }
 
  /* copy file name */
  strncpy (file->name, filename, sizeof (file->name));
 
  /* get file length */
  fseek (fp, 0, SEEK_END);
  file->length = ftell (fp);
  file->offset = 0;
  fseek (fp, 0, SEEK_SET);
 
  /* allocate memory for file data */
  file->data = (unsigned char *)malloc (file->length);
  if (!file->data)
    {
      fprintf (stderr, "Error: memory allocation failed "
	       "for \"%s\"\n", filename);
      fclose (fp);
      free (file);
      return NULL;
    }
 
  /* read whole file data */
  fread (file->data, 1, file->length, fp);
  fclose (fp);
 
  return file;
}

Cette fonction est assez simple. Elle crée une structure file_buffer_t et ouvre le fichier. Ensuite elle calcule la taille du fichier pour allouer assez d'espace afin de stocker le fichier tout entier en mémoire, puis lit le fichier. Elle retourne l'objet file_buffer_t créé et initialisé.

5.2. Lecture depuis un buffer fichier

Nous allons modifier un peu notre fonction de lecture d'une image PNG. Voici le prototype de notre nouvelle fonction :

 
Sélectionnez
gl_texture_t *ReadPNGFromMemory (const file_buffer_t *file);

Le contenu de cette fonction sera sensiblement le même que celui de ReadPNGFromFile(), je ne vais donc pas tout recopier.

On peut commencer déjà par supprimer tout ce qui concerne l'ouverture du fichier, puisque nous traitons avec le contenu du fichier directement. La vérification de la signature du fichier est légèrement modifiée comme suit :

 
Sélectionnez
/* check for valid magic number */
if (!png_check_sig (file->data, 8))
  {
    fprintf (stderr, "error: \"%s\" is not a valid PNG image!\n",
             file->name);
    return NULL;
  }

On compare directement avec les huit premiers octets du buffer. Mais il ne s'agit pas ici d'une grosse modification, simplement d'une adaptation du code.

La seule modification réside dans la manière de lire les données de l'image source. Rappelez vous dans ReadPNGFromFile(), on avait passé à libpng notre pointeur sur fichier afin qu'il utilise les fonctions d'entrée/sortie standards. Nous pouvons spécifier à libpng la fonction à appeler pour lire un bloc de données avec png_set_read_fn(). Cette fonction prend en paramètre un pointeur sur les données source (ici ce sera notre file_buffer_t) et un pointeur sur une fonction dont le prototype est :

 
Sélectionnez
void user_read_data (png_structp png_ptr, png_bytep data, png_size_t length);

Les données source que vous passez à png_set_read_fn() peuvent être ce que vous voulez, puisque c'est vous qui les manipulerez dans votre fonction « user_read_data() ».

Cette fonction user_read_data() attend que length octets soient copiés depuis la source dans data. Dans notre cas, on peut donc l'écrire ainsi :

 
Sélectionnez
void
png_read_from_mem (png_structp png_ptr, png_bytep data, png_size_t length)
{
  file_buffer_t *src = (file_buffer_t *)png_get_io_ptr (png_ptr);
 
  /* copy data from image buffer */
  memcpy (data, src->data + src->offset, length);
 
  /* advance in the file */
  src->offset += length;
}

La fonction png_get_io_ptr() nous permet de récupérer le pointeur sur la source de données que l'on a passé à libpng au moment d'appeler png_set_read_fn(). On copie ensuite les données demandées dans data et on met à jour le curseur de position (offset).

Retournons à ReadPNGFromMemory(). Remplacer les instructions png_init_io(...) et png_set_sig_buytes(...) par le code suivant :

 
Sélectionnez
/* set "png_read" callback function and give source of data */
png_set_read_fn (png_ptr, (png_voidp *)file, png_read_from_mem);

file est notre objet file_buffer_t et png_read_from_mem() la fonction définie ci-dessus. Nous ne spécifions plus à libpng la lecture des huit premiers octets contenant la signature du fichier car nous avons passé à libpng un pointeur sur le début des données du fichier (et non plus à l'adresse de base + 8). libpng devra donc les relire lui-même.

Il s'agit de l'unique modification de code pour charger une image PNG depuis une zone mémoire, le reste n'étant qu'une adaptation du code pour remplacer le pointeur sur fichier par notre objet file_buffer_t. Le fichier png_mem.c (11,0 Ko) contient le code complet de la fonction ReadPNGFromMemory(). Il contient également un exemple de gestion des erreurs personnalisée, que nous allons voir maintenant.

6. Gestion des erreurs

Comme pour la fonction de lecture des données source, il est possible de spécifier à libpng les fonctions à appeler lorsqu'une erreur ou un avertissement est rencontré, et choisir vous même l'action à effectuer dans ces situation. Par exemple, rediriger le message d'erreur vers un fichier plutôt qu'à l'écran.

Il est possible à la création du png_struct, de préciser les fonctions à appeler en cas d'erreur ou avertissement. Ces fonctions doivent être de la forme :

 
Sélectionnez
void user_error_fn (png_structp png_ptr, png_const_charp error_msg);
void user_warning_fn(png_structp png_ptr, png_const_charp warning_msg);

png_create_read_struct() prend également en paramètre un pointeur sur une structure quelconque que le programmeur pourra utiliser à sa guise, pour par exemple stocker le message d'erreur. Dans notre exemple, on peut s'en servrir pour stocker le nom du fichier que l'on est en train de charger et ainsi pouvoir dire pour quel fichier l'erreur s'est produite. Ce pointeur est accessible ensuite à l'aide de png_get_error_ptr().

Modifions l'appel à png_create_read_struct() :

 
Sélectionnez
/* create a png read struct */
png_ptr = png_create_read_struct (PNG_LIBPNG_VER_STRING,
      (png_voidp *)file->name, png_error_fn, png_warning_fn);

Nous allons maintenant écrire nos fonctions callback, png_error_fn() et png_warning_fn() :

 
Sélectionnez
void
png_error_fn (png_structp png_ptr, png_const_charp error_msg)
{
  fprintf (stderr, "png_error: %s (%s)\n", error_msg,
	   (char *)png_get_error_ptr (png_ptr));
 
  longjmp (png_jmpbuf (png_ptr), 1);
}
 
void
png_warning_fn (png_structp png_ptr, png_const_charp warning_msg)
{
  fprintf (stderr, "png_warning: %s (%s)\n", warning_msg,
	   (char *)png_get_error_ptr (png_ptr));
}

Dans cet exemple, on se contente d'afficher le message d'erreur (ou d'avertissement) sur la sortie d'erreur et le nom de fichier pour lequel il a été émis.

Vous remarquerez que pour la fonction png_error_fn() on fait un saut avec longjmp à l'endroit définit précédemment avec setjmp(). Si l'on ne le fait pas nous même, libpng exécutera notre fonction d'erreur personnalisée, puis la sienne qui exécutera le longjmp().

Remarque : pour les développeurs C++, il serait tentant ici de lancer une exception plutôt que d'utiliser le mécanisme setjmp/longjmp. C'est malheureusement impossible, car libpng est écrite en C et l'exception lancée ne sera jamais attrapée.

La gestion des erreurs est illustrée dans le second exemple de code, png_mem.c (11,0 Ko).

7. Création de la texture

Maintenant que l'on a lu et stocké les données de l'image sous un format exploitable par OpenGL, on peut aisément créer notre texture à partir de l'objet gl_texture_t :

 
Sélectionnez
/* load PNG image */
gl_texture_t *gltex = ReadPNGFromFile (filename);
if (!gltex)
  {
    fprintf (stderr, "error: couldn't load %s!\n", filename);
    exit (-1);
  }
 
/* generate texture */
glGenTextures (1, &texid);
glBindTexture (GL_TEXTURE_2D, texid);
 
/* setup texture filters */
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 
glTexImage2D (GL_TEXTURE_2D, 0, gltex->internalFormat, gltex->width, gltex->height,
              0, gltex->format, GL_UNSIGNED_BYTE, gltex->texels);
 
/* OpenGL has its own copy of texture data */
free (gltex->texels);
free (gltex);

D'abord on utilise notre fonction ReadPNGFromFile() pour initialiser gltex, un objet de type gl_texture_t. En cas de succès de la lecture de l'image, on crée la texture OpenGL avec les fonctions faites pour. Vérifiez bien que les dimensions de votre image sont multiple de 2 ou alors il vous faudra utiliser gluScaleImage() ou bien utiliser gluBuild2DMipmaps() à la place de glTexImage2D() (dans ce cas il faut aussi modifier le paramètre de filtrage associé à GL_TEXTURE_MAG_FILTER).

Une fois la texture créée, on libère la mémoire allouée pour stocker les données des pixels de l'image car OpenGL possède sa propre copie de ces données, il est donc inutile d'encombrer la mémoire avec ça.

L'utilisation de ReadPNGFromMemory() est quasiment identique. Il y juste une étape en plus : la lecture du fichier dans un buffer.

8. Conclusion

Nous avons vu dans cet article deux manières de lire une image PNG à l'aide de la bibliothèque libpng : depuis un fichier, avec les fonctions d'entrée/sortie standards, et depuis une zone mémoire contenant la totalité du fichier. Nous avons ensuite vu rapidement la gestion personnalisée des erreurs.

Deux programmes d'exemples sont disponibles :

  • png.c (9,0 Ko), lecture d'une image PNG depuis un fichier ;
  • png_mem.c (11,0 Ko), lecture d'une image PNG depuis une zone mémoire et gestion personnalisée des erreurs ;

À noter que la lecture des images JPEG, à l'aide de la bibliothèque libjpeg, ressemble beaucoup à la lecture d'images PNG.

Si vous avez des remarques sur cet article, vous pouvez me contacter par mail à tfc_duke (CHEZ) club-internet (POINT) fr.

Version PDF : télécharger (67.3 Ko)


Graphics interchange format

Logo Creative CommonsLogo some rights reserved Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).