Truevision Targa

A propos du code

Le code dans cet article a été écrit avec Code::Blocks et la SDL 2.
Vous pouvez trouver ici un guide pour installer ces logiciels.
Bien qu'il soit basé sur la SDL, je n'utilise pas ses fonctions directement. J'ai écrit une petite bibliothèque
avec quelques fonctions basiques pour faciliter la compréhension et la portabilité dans un autre langage.
Vous pouvez en apprendre plus sur cette bibliothèque ici.

Description du format

Le Truevision Targa est l'un des formats d'image les plus simples.
Il gère les images palettisées ou truecolor de 8, 15, 24 ou 32 bits et il supporte le compression RLE.
Il y a 2 versions de ce format la 1.0 et la 2.0. Je vais décrire ici la version 1.0.
La version 2.0 n'ajoute que des données additionnelles à la fin du fichier, principalement pour ajouter une
description ou le nom de l'auteur.

Le fichier image contient 4 parties principales.

Je vais décrire le chargement de tous les formats supportés par ces fichiers.
On stockera l'image dans un tableau d'objets "Color". "Color" étant la classe que j'ai écrite pour ma lib SDL.
Je vais aussi décrire la sauvegarde de ces tableau de Color. On les sauvera en truecolor 32 bits, car ma lib
n'utilise pas vraiment de palette pour le moment.

Fonctions de fichiers

Avant de nous plonger dans les fonctions de chargement, pour rendre les choses plus claires, je veux éviter les
tests d'erreur autant que possible.
Alors j'ai écrit un ensemble de fonctions utilitaires pour les fichiers qui gèrent toutes les erreurs.
Elles ne sont pas très complexes. elles ne font qu'appeler les fonctions de fichiers C standard (fopen, fclose,
fread, ...) et si une erreur apparait, elles affichent un message et stoppent le programme.
Voici le fichier d'entête pour ces fonctions:
		#ifndef CFILE_H
		#define CFILE_H

		#include <stdio.h>
		#include <SDL.h>

		class CFile
		{
		public:
			struct SFile
			{
				FILE*   file;
				char*   fileName;
			};

			SFile*      open(const char* fileName, const char* mode);
			void        close(SFile* f);
			void        seek(SFile* f, long int offset, int origin);
			long int    tell(SFile* f);

			void    read(SFile* f, void* dest, int nbBytes);
			Uint8   readU8(SFile* f);
			Sint8   readS8(SFile* f);
			Uint16  readU16(SFile* f);
			Sint16  readS16(SFile* f);
			Uint32  readU32(SFile* f);
			Sint32  readS32(SFile* f);
			Uint64  readU64(SFile* f);
			Sint64  readS64(SFile* f);
			float   readFloat(SFile* f);
			double  readDouble(SFile* f);

			void    write(SFile* f, const void* src, int nbBytes);
			void    writeU8(SFile* f, const Uint8 value);
			void    writeS8(SFile* f, const Sint8 value);
			void    writeU16(SFile* f, const Uint16 value);
			void    writeS16(SFile* f, const Sint16 value);
			void    writeU32(SFile* f, const Uint32 value);
			void    writeS32(SFile* f, const Sint32 value);
			void    writeU64(SFile* f, const Uint64 value);
			void    writeS64(SFile* f, const Sint64 value);
			void    writeFloat(SFile* f, const float value);
			void    writeDouble(SFile* f, const double value);

		private:
			void    printError(const char* error, const char* fileName);
			void    printErrorAndClose(const char* error, SFile* f);
		};

		extern CFile file;

		#endif // CFILE_H
				
Si vous voulez les regarder de plus près vous pouvez télécharger les sources ici.

Chargement: l'entête

l'entête contient les champs suivants:

Taille (bits) Nom Description
8 IDLength Nombres d'octets dans la zone de description du fichier qu'on va sauter.
8 colorMapType 0 s'il n'y a pas de palette.
1 s'il y a une palette.
8 imageType 0: Pas d'image. On ne gérera pas ce cas.
1: Indexé. Les données de l'image seront des index sur les couleurs de la palette.
2: Truecolor. Les données de l'image contiennent directement les valeurs RGB(A) des pixels.
3: Tons de gris. Pas de palette. Les données de l'image contiennent une valeur de gris pour chaque pixel.
9: Indexé, compressé en RLE.
10: Truecolor, compressé en RLE.
11: Tons de gris, compressé en RLE.
16 colorMap_firstEntryIndex Index de la première couleur dans la palette.
16 colorMap_length Nombre de couleurs dans la palette.
8 colorMap_entrySize Nombre de bits d'une couleur dans la palette.
Peut être 15, 16, 24 ou 32 bits.
15 et 16 bits sont les mêmes. Il y a 5 bits par composante, le 16ème n'est pas utilisé.
16 xOrigin Position x de l'image à l'écran. On considérera toujours que c'est 0.
16 yOrigin Position y de l'image à l'écran. On considérera toujours que c'est 0.
16 imageWidth Largeur de l'image en pixels.
16 imageHeight Hauteur de l'image en pixels.
8 pixelDepth Taille de chaque pixel dans la zone de données de l'image en bits.
Ca peut être soit la taille des composantes du pixels, la taille de l'index dans la palette,
ou la taille de la valeur de gris.
Ce champ peut prendre les valeurs 8, 16, 24 ou 32.
8 flags
  • bits 0 à 3: nombres de bits d'alpha. Ca peut être soit 0, 1 ou 8.
    La valeur 1 est seulement utilisée pour les images 16 bits, mais on va l'oublier parce que la doc
    n'est pas très précise à ce sujet.
  • bit 4: direction horizontale de l'image. 0: de gauche à droite, 1: de droite à gauche
  • bit 5: direction verticale de l'image. 0: de bas en haut, 1: de haut en bas
Contrairement à la plupart des format d'image, le Targa peut stocker l'image dans différentes orientations.
Par défaut c'est "de gauche à droite" et "de bas en haut".
Ce qui veut dire que le premier pixel est en bas à gauche de l'écran.

J'écrirai tout le code relatif au format Targa dans une classe TGA.
L'entête de cette classe contient quelques enums pour diverses valeurs définies dans le tableau ci-dessus et une
structure avec tous les champs de l'entête.
		#ifndef TGA_H
		#define TGA_H

		#include "../Graphics.h"
		#include "../CFile.h"

		class TGA
		{
		public:
			Color*  load(const char* filename, int* width, int* height);

		private:
			enum EHDir
			{
				eLeftToRight,
				eRightToLeft
			};

			enum EVDir
			{
				eTopToBottom,
				eBottomToTop
			};

			enum EImageType
			{
				eNoImage = 0,
				eIndexed = 1,
				eTrueColor = 2,
				eGray = 3,
				eIndexedRLE = 9,
				eTrueColorRLE = 10,
				eGrayRLE = 11
			};

			// header datas
			struct SHeader
			{
				Uint8   IDLength;
				Uint8   colorMapType;
				Uint8   imageType;
				Uint16  colorMap_firstEntryIndex;
				Uint16  colorMap_length;
				Uint8   colorMap_entrySize;
				Uint16  xOrigin;
				Uint16  yOrigin;
				Uint16  imageWidth;
				Uint16  imageHeight;
				Uint8   pixelDepth;
				Uint8   alphaBits;
				EHDir   hDirection;
				EVDir   vDirection;
			};

			void    printError(const char* error, CFile::SFile* f);

			void    readHeader(CFile::SFile* f, SHeader* h);
			void    printHeader(SHeader* h);
		};

		extern TGA tga;

		#endif // TGA_H
				
Maintenant je vais expliquer les quelques fonctions.
load() est le point d'entrée principal pour charger une image.
Elle prend en entrée le nom du fichier qu'on veut charger, et renvoie le tableau de Color, la largeur et la hauteur
de l'image.
		Color* TGA::load(const char* filename, int* width, int* height)
		{
			CFile::SFile*   f = file.open(filename, "rb");

			SHeader header;
			readHeader(f, &header);
			printHeader(&header);

			Color* image = NULL;
			*width  = header.imageWidth;
			*height = header.imageHeight;

			file.close(f);
			return image;
		}
				
printError() est une petite fonction qui affiche un message d'erreur et quitte le programme.
		void TGA::printError(const char* error, CFile::SFile* f)
		{
			printf("TGA %s : %s\n", f->fileName, error);
			file.close(f);
			exit(EXIT_FAILURE);
		}
				
readHeader est la fonction qui lit chaque champ de l'entête en suivant le tableau ci-dessus, et qui les stocke dans
la structure SHeader.
Pour la plupart des champs je vérifie que la valeur qu'on obtient suit bien les règles décrites dans le tableau.
Si ce n'est pas le cas, on affiche un message d'erreur.
		void TGA::readHeader(CFile::SFile* f, SHeader* h)
		{
			h->IDLength = file.readU8(f);

			h->colorMapType = file.readU8(f);
			if (h->colorMapType > 1)
				printError("Wrong format", f);

			h->imageType = file.readU8(f);

			if (h->imageType != eIndexed &&
			    h->imageType != eTrueColor &&
			    h->imageType != eGray &&
			    h->imageType != eIndexedRLE &&
			    h->imageType != eTrueColorRLE &&
			    h->imageType != eGrayRLE)
			{
				printError("Wrong format", f);
			}

			h->colorMap_firstEntryIndex = file.readU16(f);
			h->colorMap_length = file.readU16(f);
			h->colorMap_entrySize = file.readU8(f);

			if (h->colorMapType == 1 && h->colorMap_length != 0)
			{
				if (h->colorMap_entrySize != 15 &&
				    h->colorMap_entrySize != 16 &&
				    h->colorMap_entrySize != 24 &&
				    h->colorMap_entrySize != 32)
				{
					printError("Wrong format", f);
				}
			}

			h->xOrigin = file.readU16(f);
			h->yOrigin = file.readU16(f);
			h->imageWidth  = file.readU16(f);
			h->imageHeight = file.readU16(f);
			h->pixelDepth = file.readU8(f);

			if (h->pixelDepth != 8 &&
			    h->pixelDepth != 16 &&
			    h->pixelDepth != 24 &&
			    h->pixelDepth != 32)
			{
				printError("Wrong format", f);
			}

			Uint8 flags = file.readU8(f);

			if ((flags & 0xC0) != 0)
				printError("Wrong format", f);

			h->alphaBits = flags & 0xF;

			if (h->alphaBits != 0 &&
			    h->alphaBits != 1 &&
			    h->alphaBits != 8)
			{
				printError("Wrong format", f);
			}

			if ((flags & 0x10) == 0)
				h->hDirection = eLeftToRight;
			else
				h->hDirection = eRightToLeft;

			if ((flags & 0x20) == 0)
				h->vDirection = eBottomToTop;
			else
				h->vDirection = eTopToBottom;

			if (h->imageType == eIndexed && h->colorMap_length == 0)
				printError("Wrong format", f);

			if (h->imageType == eIndexedRLE && h->colorMap_length == 0)
				printError("Wrong format", f);
		}
				
Et pour tester ce programme j'ai écrit une fonction printHeader qui affiche toutes les valeurs qu'on a lues dans le
fichier.
		void TGA::printHeader(SHeader* h)
		{
			printf("IDLength: %d\n", h->IDLength);
			printf("colorMapType: %d\n", h->colorMapType);
			printf("imageType: %d\n", h->imageType);
			printf("colorMap_firstEntryIndex: %d\n", h->colorMap_firstEntryIndex);
			printf("colorMap_length: %d\n", h->colorMap_length);
			printf("colorMap_entrySize: %d\n", h->colorMap_entrySize);
			printf("xOrigin: %d\n", h->xOrigin);
			printf("yOrigin: %d\n", h->yOrigin);
			printf("imageWidth: %d\n", h->imageWidth);
			printf("imageHeight: %d\n", h->imageHeight);
			printf("pixelDepth: %d\n", h->pixelDepth);
			printf("alphaBits: %d\n", h->alphaBits);
			printf("hDirection: %s\n", (h->hDirection == eLeftToRight ? "left to right" : "right to left"));
			printf("vDirection: %s\n", (h->vDirection == eTopToBottom ? "top to bottom" : "bottom to top"));
		}
				
Enfin la fonction main appelle simplement la fonction load:
		int main(int argc, char* argv[])
		{
			// init the window
			int width, height;

			Color* image = tga.load("image.tga", &width, &height);

			if (image != NULL)
				delete[] image;

			return EXIT_SUCCESS;
		}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Avec l'image d'exemple que j'ai mise dans les sources, la sortie de ce programme est:
	IDLength: 0
	colorMapType: 1
	imageType: 1
	colorMap_firstEntryIndex: 0
	colorMap_length: 256
	colorMap_entrySize: 24
	xOrigin: 0
	yOrigin: 0
	imageWidth: 800
	imageHeight: 574
	pixelDepth: 8
	alphaBits: 0
	hDirection: left to right
	vDirection: bottom to top
				

Chargement: la palette

On va maintenant charger la palette de couleurs, alors on va ajouter ces lignes à la fonction load:
		Color* TGA::load(const char* filename, int* width, int* height)
		{
			[...]
			// read header
			[...]

			// skip image ID
			if (header.IDLength != 0)
				file.seek(f, header.IDLength, SEEK_CUR);

			// read palette
			Color*  palette = readPalette(f, &header);
			printPalette(&header, palette);

			// read image
			[...]
		}
				
Le nombre de couleurs de la palette est donné par colorMap_length.
colorMap_entrySize définit le format des couleurs.

Si colorMap_entrySize est 15 ou 16 bits, on lit un mot de 16 bits.
Les composantes rouge, vert et bleu de la couleur ont chacune 5 bits. Le 16ème bit n'est pas utilisé.
Ce tableau montre quels bits sont utilisés pour chaque composante dans le mot de 16 bits:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

Si colorMap_entrySize est 24 bits, les composantes bleu, vert et rouge sont stockées dans cet ordre sur 3 octets.

octet 0 octet 1 octet 2

Si colorMap_entrySize est 32 bits, un 4ème octet est ajouté pour la composante alpha.

octet 0 octet 1 octet 2 octet 3

La fonction readPalette() va lire toutes les couleurs et les stocker dans un tableau de Color.
Si le type de l'image n'a pas besoin de palette, on saute simplement les données dans le fichier.
		Color* TGA::readPalette(CFile::SFile* f, SHeader* h)
		{
			Color*  palette = NULL;
			int bytesPerColor = (h->colorMap_entrySize + 7) / 8;

			if (h->imageType == eIndexed || h->imageType == eIndexedRLE)
			{
				palette = new Color[h->colorMap_length];

				for (int i = 0; i < h->colorMap_length; i++)
				{
					if (h->colorMap_entrySize == 15 || h->colorMap_entrySize == 16)
					{
						Uint16 value = file.readU16(f);
						palette[i].b = ( value        & 0x1F) << 3;
						palette[i].g = ((value >>  5) & 0x1F) << 3;
						palette[i].r = ((value >> 10) & 0x1F) << 3;
						palette[i].a = 255;
					}
					else if (h->colorMap_entrySize == 24)
					{
						palette[i].b = file.readU8(f);
						palette[i].g = file.readU8(f);
						palette[i].r = file.readU8(f);
						palette[i].a = 255;
					}
					else if (h->colorMap_entrySize == 32)
					{
						palette[i].b = file.readU8(f);
						palette[i].g = file.readU8(f);
						palette[i].r = file.readU8(f);
						palette[i].a = file.readU8(f);
					}
				}
			}
			else
			{
				file.seek(f, h->colorMap_length * bytesPerColor, SEEK_CUR);
			}

			return palette;
		}
				
Comme pour l'entête j'ai écrit une petite fonction pour afficher les valeurs qu'on a lues.
		void TGA::printPalette(SHeader* h, Color* palette)
		{
			for (int i = 0; i < h->colorMap_length; i++)
				printf("color %d: (r:%02x, g:%02x, b:%02x, a:%02x)\n", i, palette[i].r, palette[i].g, palette[i].b, palette[i].a);
		}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Avec l'image d'exemple dans les sources vous verrez ça.
	color 0: (r:04, g:b3, b:ab, a:ff)
	color 1: (r:bc, g:a8, b:95, a:ff)
	color 2: (r:2d, g:83, b:d3, a:ff)
	color 3: (r:02, g:a3, b:a0, a:ff)
	color 4: (r:b0, g:94, b:82, a:ff)
	color 5: (r:25, g:26, b:28, a:ff)
	color 6: (r:c5, g:bc, b:ab, a:ff)
	color 7: (r:35, g:3b, b:40, a:ff)
	color 8: (r:30, g:67, b:b2, a:ff)
	color 9: (r:3a, g:5c, b:90, a:ff)
	color 10: (r:3c, g:51, b:67, a:ff)
	color 11: (r:94, g:79, b:6b, a:ff)
	color 12: (r:07, g:86, b:86, a:ff)
	color 13: (r:33, g:a4, b:e9, a:ff)
	color 14: (r:c3, g:cf, b:cf, a:ff)
	color 15: (r:6c, g:90, b:ab, a:ff)
	color 16: (r:00, g:00, b:00, a:ff)
	color 17: (r:00, g:00, b:00, a:ff)
	...
				
Note: c'est une image de 16 couleurs que j'ai créée avec The Gimp mais il a décidé de sauver une palette de 256
couleurs pleine de 0.

Chargement: données d'image non compressées

Maintenant on va lire les données d'image dans un fichier qui n'est pas compressé en RLE.
La fonction load va maintenant appeler une fonction readImage:
		Color* TGA::load(const char* filename, int* width, int* height)
		{
			[...]

			// read image
			Color* image = readImage(f, &header, palette);
			*width  = header.imageWidth;
			*height = header.imageHeight;

			[...]
		}
				
Dans cette fonction on va charger les données d'image du fichier les les stocker dans un tableau imageData:
		Color*  TGA::readImage(CFile::SFile* f, SHeader* h, Color* palette)
		{
			int bytesPerPixel = (h->pixelDepth + 7) / 8;
			Color*  image = new Color[h->imageWidth * h->imageHeight];
			Uint8*  imageData = new Uint8[h->imageWidth * h->imageHeight * bytesPerPixel];
			int pixelIndex = 0;

			if (h->imageType == eIndexed ||
			    h->imageType == eTrueColor ||
			    h->imageType == eGray)
			{
				file.read(f, imageData, h->imageWidth * h->imageHeight * bytesPerPixel);
			}
			else
			{
				// compressed image
			}
				
On boucle sur chaque pixel de l'image:
			for (int y = 0; y < h->imageHeight; y++)
				for (int x = 0; x < h->imageWidth; x++)
				{
					Color   c;
				
On lit le pixel suivant dans imageData:
					// read the next pixel
					Uint32 pixel = 0;

					for (int i = 0; i < bytesPerPixel; i++)
						pixel |= imageData[pixelIndex++] << (i * 8);
				
Et maintenant on va décoder sa couleur.
Le premier cas est celui d'une image indexée. Alors le pixel qu'on lit est l'index d'une couleur dans la palette:
					// decode color
					if (h->imageType == eIndexed || h->imageType == eIndexedRLE)
					{
						c = palette[pixel - h->colorMap_firstEntryIndex];
					}
				
Dans le deuxième cas, on lit une image en tons de gris. Le pixel dans ce cas est un niveau de gris.
On met simplement ce niveau dans les composantes rouge, vert et bleu de notre couleur.
					else if (h->imageType == eGray || h->imageType == eGrayRLE)
					{
						int g = pixel >> (h->pixelDepth - 8);
						c = Color(g, g, g, 255);
					}
				
Dans le dernier cas on a une image truecolor. Ici la couleur est dans les mêmes formats que pour la palette:
					else
					{
						if (bytesPerPixel == 2)
						{
							c.b = ( pixel        & 0x1F) << 3;
							c.g = ((pixel >>  5) & 0x1F) << 3;
							c.r = ((pixel >> 10) & 0x1F) << 3;
							c.a = 255;
						}
						else if (bytesPerPixel == 3)
						{
							c.b =  pixel        & 0xFF;
							c.g = (pixel >>  8) & 0xFF;
							c.r = (pixel >> 16) & 0xFF;
							c.a = 255;
						}
						else if (bytesPerPixel == 4)
						{
							c.b =  pixel        & 0xFF;
							c.g = (pixel >>  8) & 0xFF;
							c.r = (pixel >> 16) & 0xFF;
							c.a = (pixel >> 24) & 0xFF;
						}
					}
				
Enfin pour trouver l'endroit où l'on va écrire cette couleur, il faut qu'on gère l'orientation de l'image:
					// handle image directions
					int destX, destY;

					if (h->hDirection == eLeftToRight)
						destX = x;
					else
						destX = h->imageWidth - 1 - x;

					if (h->vDirection == eTopToBottom)
						destY = y;
					else
						destY = h->imageHeight - 1 - y;

					image[destY * h->imageWidth + destX] = c;
				}

			return image;
		}
				
Maintenant on va afficher cette image à l'écran dans la fonction main():
		int main(int argc, char* argv[])
		{
			// init the window
			int width, height;

			Color* image = tga.load("image.tga", &width, &height);

			gfx.init("TGA Reader", width, height);
			gfx.init2D();
			gfx.clearScreen(Color(0, 0, 0));

			// copy image to the screen
			for (int y = 0; y < height; y++)
				for (int x = 0; x < width; x++)
					gfx.setPixel(x, y, image[y * width + x]);

			// wait till the end
			while (sys.isQuitRequested() == false)
			{
				gfx.render();
				sys.processEvents();
				sys.wait(50);
			}

			gfx.quit();

			if (image != NULL)
				delete[] image;

			return EXIT_SUCCESS;
		}
				
		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Vous devriez voir l'image d'exemple maintenant:

Chargement: décompression

Comme on l'a dit l'image peut être compressée en RLE. J'ai déjà écrit un article à propos de cette méthode de
compression.
Les données compressées peuvent être vues comme une suite de paquets.
Chaque paquet contient un compteur sur 1 octet suivi d'un ou de plusieurs pixels.
Les données des pixels sont dans le même format que la palette ou les données d'image non compressées, donc ça peut
être des valeurs de 8, 16, 24 ou 32 bits.

Si le bit de poids fort du compteur est 0, il est suivi par N pixels à recopier tels quels dans sortie.
Où N est la valeur du compteur + 1.
Voici un exemple:

0x03 pixel 1 pixel 2 pixel 3 pixel 4

Si le bit de poids fort du compteur est 1, il est suivi d'un seul pixel à répéter N fois dans la sortie.
Ici N est la valeur des 7 bits de poids faible du compteur + 1.
Voici un exemple:

0x83 pixel

Le pixel sera répété 4 fois dans ce cas.

Donc la fonction de décompression est assez simple.
On commence par calculer la taille des données décompressées.
On considère qu'une mémoire de cette taille a été alloué en dehors de cette fonction pour stocker la sortie.
		void TGA::uncompress(CFile::SFile* f, SHeader* h, Uint8* imageData, int bytesPerPixel)
		{
			int uncompSize = h->imageWidth * h->imageHeight * bytesPerPixel;
			int index = 0;
				
Ensuite on va boucler jusqu'à ce qu'on atteigne cette taille
			while (index < uncompSize)
			{
				
On lit le compteur et on en extrait le nombre de pixels
				Uint8   counter = file.readU8(f);
				Uint8   counter2 = (counter & 0x7f) + 1;
				
Si décompresser ce paquet nous ferait déborder de la mémoire de sortie, on affiche une erreur.
				if (index + counter2 * bytesPerPixel > uncompSize)
					printError("Wrong format", f);
				
Si le bit de poids fort du compteur est 0, on écrit les "N" pixels suivants dans la sortie.
				if ((counter & 0x80) == 0)
				{
					// raw packet
					file.read(f, &imageData[index], counter2 * bytesPerPixel);
					index += counter2 * bytesPerPixel;
				}
				
Si le bit de poids fort est 1, on lit le pixel suivant, et on l'écrit "N" fois dans la sortie.
				else
				{
					// run-length packet
					static Uint8 pixel[4];

					file.read(f, pixel, bytesPerPixel);

					for (int i = 0; i < counter2; i++)
						for (int j = 0; j < bytesPerPixel; j++)
							imageData[index++] = pixel[j];
				}
			}
		}
				
Et on a fini.
Maintenant il ne faut pas oublier d'appeler cette fonction au début de readImage:
		Color*  TGA::readImage(CFile::SFile* f, SHeader* h, Color* palette)
		{
			[...]

			if (h->imageType == eIndexed ||
			    h->imageType == eTrueColor ||
			    h->imageType == eGray)
			{
				file.read(f, imageData, h->imageWidth * h->imageHeight * bytesPerPixel);
			}
			else
			{
				uncompress(f, h, imageData, bytesPerPixel);
			}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				
Le code source contient une image d'exemple qui est lue par le programme.

Sauvegarde: image non compressée

On va maintenant écrire une fonction de sauvegarde pour sauver une image non compressée.
Comme ma lib SDL ne gère les images que comme un tableau d'objets Color, on va sauver ça dans un fichier truecolor
32 bits.
Donc voici la fonction de sauvegarde:
		void TGA::save(const char* filename, int width, int height, Color* image, bool compress)
		{
			CFile::SFile*   f = file.open(filename, "wb");

			writeHeader(f, width, height, compress);
			writeImage(f, width, height, image, compress);

			file.close(f);
		}
				
Comme on a choisi un format spécifique, la fonction writeHeader est simple:
		void TGA::writeHeader(CFile::SFile* f, int width, int height, bool compress)
		{
			file.writeU8(f, 0); // IDLength
			file.writeU8(f, 0); // colorMapType

			// imageType
			if (compress == false)
				file.writeU8(f, 2);
			else
				file.writeU8(f, 10);

			file.writeU16(f, 0); // colorMap_firstEntryIndex
			file.writeU16(f, 0); // colorMap_length
			file.writeU8(f, 0);  // colorMap_entrySize

			file.writeU16(f, 0);      // xOrigin
			file.writeU16(f, 0);      // yOrigin
			file.writeU16(f, width);  // imageWidth
			file.writeU16(f, height); // imageHeight
			file.writeU8(f, 32);      // pixelDepth

			file.writeU8(f, 8); // flags: 8 bits of alpha, left to right, bottom to top
		}
				
La fonction writeImage sauve simplement un tableau de pixels:
		void TGA::writeImage(CFile::SFile* f, int width, int height, Color* image, bool compress)
		{
			if (compress == false)
			{
				for (int y = 0; y < height; y++)
					for (int x = 0; x < width; x++)
					{
						Color c = image[(height - 1 - y) * width + x];
						writePixel(f, c);
					}
			}
			else
			{
				// compress
			}
		}
				
Et voici la fonction pour pour écrire un pixel:
		void TGA::writePixel(CFile::SFile* f, Color c)
		{
			file.writeU8(f, c.b);
			file.writeU8(f, c.g);
			file.writeU8(f, c.r);
			file.writeU8(f, c.a);
		}
				
Maintenant pour tester ça on va charger une image, la sauver sous un autre nom, libérer la mémoire de l'image et
recharger l'image qu'on a sauvée. Si tout s'est bien passé vous devriez voir la même image à l'écran.
		int main(int argc, char* argv[])
		{
			// init the window
			int width, height;

			Color* image = tga.load("image2.tga", &width, &height);
			tga.save("test.tga", width, height, image, false);
			delete[] image;
			image = tga.load("test.tga", &width, &height);

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				

Sauvegarde: compression

On ne va pas compresser les données de l'image d'un seul coup, mais ligne par ligne.
Ce n'est pas une exigence de la spécification du Targa 1.0 mais la doc conseille ça pour la version 2.0.
Donc la fonction writeImage va contenir une boucle:
		void TGA::writeImage(CFile::SFile* f, int width, int height, Color* image, bool compress)
		{
			if (compress == false)
			{
				[...]
			}
			else
			{
				for (int y = 0; y < height; y++)
				{
					Color* line = &image[(height - 1 - y) * width];
					compressLine(f, line, width);
				}
			}
		}
				
Pour compresser une ligne, je vais utiliser la fonction de compression que j'ai décrite dans mon article sur la
compression RLE. Je l'ai juste légèrement modifiée pour écrire le résultat directement dans le fichier.
		#define TGA_MIN_RUN     3
		#define TGA_MAX_RUN     128
		#define TGA_MIN_STRING  1
		#define TGA_MAX_STRING  128

		void TGA::compressLine(CFile::SFile* f, Color* input, int inputSize)
		{
			int stringStart = 0;
			int runStart = 0;
			int currentPixel = 1;
			bool isInRun = false;

			while (true)
			{
				if (isInRun == false)
				{
					// string mode
					if (input[currentPixel] != input[runStart])
						runStart = currentPixel;

					int stringLength = runStart - stringStart;
					int runLength    = currentPixel + 1 - runStart;

					if (currentPixel == inputSize ||
					    runLength    == TGA_MIN_RUN ||
					    stringLength == TGA_MAX_STRING)
					{
						// emit string
						if (stringLength >= TGA_MIN_STRING)
						{
							file.writeU8(f, stringLength - 1);

							for (int i = 0; i < stringLength; i++)
								writePixel(f, input[stringStart++]);
						}

						// go to run mode
						if (runLength == TGA_MIN_RUN)
							isInRun = true;
					}
				}
				else
				{
					// run mode
					stringStart = currentPixel;

					int runLength = currentPixel - runStart;

					if (input[currentPixel] != input[runStart] ||
					    currentPixel == inputSize ||
					    runLength == TGA_MAX_RUN)
					{
						// emit run
						file.writeU8(f, 0x80 + runLength - 1);
						writePixel(f, input[runStart]);
						runStart = currentPixel;

						// go to string mode
						isInRun = false;
					}
				}

				if (currentPixel == inputSize)
					break;

				currentPixel++;
			}
		}

		Télécharger le code source
		Télécharger l'exécutable pour Windows
				

Liens

Documentation officielle du format

Fichiers d'exemple