Truevision Targa
About the code
Description of the format
File functions
#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
If you want to take a deeper look at them you can download the files here.
Loading: the header
Size in bits | Name | Description |
---|---|---|
8 | IDLength | Number of bytes of the file description area that we will skip. |
8 | colorMapType | 0 if there is no palette. 1 if there is a palette. |
8 | imageType | 0: No image. We won't handle this case. 1: Indexed. Means the image datas contains indexes to the palette colors. 2: Truecolor. The image datas contains the direct RGB(A) values of the pixels. 3: Grayscale. No palette. The image datas contains a gray value for each pixel. 9: Indexed, RLE compressed. 10: Truecolor, RLE compressed. 11: Grayscale, RLE compressed. |
16 | colorMap_firstEntryIndex | Index of the first color in the palette. |
16 | colorMap_length | Number of colors in the palette. |
8 | colorMap_entrySize | Number of bits of a color in the palette. Can be 15, 16, 24 or 32 bits. 15 and 16 bits are the same. There are 5 bits per component, the 16th is unused. |
16 | xOrigin | x position of the image on the screen. We will always assume it's 0. |
16 | yOrigin | y position of the image on the screen. We will always assume it's 0. |
16 | imageWidth | Horizontal size of the image in pixels. |
16 | imageHeight | Vertical size of the image in pixels. |
8 | pixelDepth | Size of the datas for each pixel in the "image datas" area in bits. It can be either the size of the pixel components, the size of the index in the palette, or the size of the gray value. This field can take the values 8, 16, 24 or 32. |
8 | flags |
and "bottom to top". Meaning that the first pixel is at the bottom-left of the screen. |
#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
Now I will explain the few functions.
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() is a small function that prints an error message and exits the program.
void TGA::printError(const char* error, CFile::SFile* f)
{
printf("TGA %s : %s\n", f->fileName, error);
file.close(f);
exit(EXIT_FAILURE);
}
readHeader is the function that reads each field of the header following the table above, and stores them in the
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);
}
And to test this program I wrote a printHeader function that displays all the values we read from the file.
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"));
}
Finally the main function simply calls the TGA::load function:
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;
}
Download source code
Download executable for Windows
With the sample image I included in the sources, the output of this program is:
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
Loading: the 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
[...]
}
The number of colors in the palette is given by colorMap_length.15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 0 | byte 1 | byte 2 |
byte 0 | byte 1 | byte 2 | byte 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;
}
As for the header, I wrote a small function to print the value we read.
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);
}
Download source code
Download executable for Windows
With the sample image in the sources you will see that.
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: it's a 16 color image that I created with "The Gimp", but it decided to save a 256 color palette full of 0.
Loading: uncompressed image data
Color* TGA::load(const char* filename, int* width, int* height)
{
[...]
// read image
Color* image = readImage(f, &header, palette);
*width = header.imageWidth;
*height = header.imageHeight;
[...]
}
In this function we will load the image data from the file and store it in an imageData array:
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
}
We loop over each pixel of the image:
for (int y = 0; y < h->imageHeight; y++)
for (int x = 0; x < h->imageWidth; x++)
{
Color c;
We read the next pixel in imageData:
// read the next pixel
Uint32 pixel = 0;
for (int i = 0; i < bytesPerPixel; i++)
pixel |= imageData[pixelIndex++] << (i * 8);
And now we will decode its color.
// decode color
if (h->imageType == eIndexed || h->imageType == eIndexedRLE)
{
c = palette[pixel - h->colorMap_firstEntryIndex];
}
In the second case we read a grayscale image. The pixel in this case is a gray level.
else if (h->imageType == eGray || h->imageType == eGrayRLE)
{
int g = pixel >> (h->pixelDepth - 8);
c = Color(g, g, g, 255);
}
In the last case, we have a truecolor image. Here the color is in the same format as the 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;
}
}
Finally, to find the place where we will write this color we have to handle the image orientation:
// 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;
}
Now we will display this image on the screen in the main() function:
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;
}
Download source code
Download executable for Windows
You should now see the sample image:
Loading: uncompressing
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;
Then we will loop until we reach this size
while (index < uncompSize)
{
We read the counter and extract the number of pixels from it
Uint8 counter = file.readU8(f);
Uint8 counter2 = (counter & 0x7f) + 1;
If uncompressing this packet would make us overflow the output memory, we print an error.
if (index + counter2 * bytesPerPixel > uncompSize)
printError("Wrong format", f);
If the most significant bit of the counter is 0, we write the "N" following pixels to the output.
if ((counter & 0x80) == 0)
{
// raw packet
file.read(f, &imageData[index], counter2 * bytesPerPixel);
index += counter2 * bytesPerPixel;
}
If the most significant bit is 1, we read the following pixel, and write it "N" times to the output.
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];
}
}
}
And we're done.
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);
}
Download source code
Download executable for Windows
The source code contains a sample compressed image that is read by the program.
Saving: uncompressed image
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);
}
As we chose a specific format, the writeHeader function is 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
}
The writeImage function simply saves an array of 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
}
}
And here is the function to write one 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);
}
Now to test that, in the main function, we will load an image, save it to another name, delete the image memory and
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);
Download source code
Download executable for Windows
Saving: compressing
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);
}
}
}
To compress the image data I will use the compression function I described in my article about RLE compression.
I only slightly modified it to write the result directly to the file.
#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++;
}
}
Download source code
Download executable for Windows