Truevision Targa
A propos du code
Description du format
Fonctions de fichiers
#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
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 |
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. |
#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.
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
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
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
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.15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
octet 0 | octet 1 | octet 2 |
octet 0 | octet 1 | octet 2 | octet 3 |
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
Chargement: données d'image non compressées
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.
// 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.
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
0x03 | pixel 1 | pixel 2 | pixel 3 | pixel 4 |
0x83 | pixel |
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.
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
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
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
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
#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