I'm diving into various Image reading and writing methods and writing my own. I decided to tackle TGA/Targa. I got reading working flawlessly. It reads both compressed and decompressed .TGA files. I got writing decompressed .TGA files working.

However, I cannot figure out how to write Compressed .TGA files. I read it like:

If a 1-byte chunk header "X" is less than 127, the amount of pixels to read is: (X - 1) before I hit another 1-byte header. If X is above 127, then it is the amount of times a pixel is repeated consecutively. So (X - 127) is the amount of times I copy that pixel into the buffer. I minused 127 from X inorder to get rid of the 1-byte header.

Now, I have a problem reversing this algorithm and writing it back to the file. I have no clue where to start. I read wikipedia and it stated that the algorithm is the RLE-Packbits algorithm. I'm not sure how true that is, but I do know it is RLE because it repeats?

I put the whole source code below so any one willing to help can compile it. The Loading/Reading works fine. I only need help "saving as Compressed" (Line 84).

Any help is appreciated.

#include <iostream>
#include <vector>
#include <fstream>
#include <cstring>
#include <stdexcept>

typedef union RGB
{
    uint32_t Color;
    struct
    {
        unsigned char B, G, R, A;
    } RGBA;
} *PRGB;

class Tga
{
    private:
        std::vector<unsigned char> ImageData;
        uint32_t width, height, size, BitsPerPixel;

    public:
        Tga(const char* FilePath);
        void Save(const char* FilePath);
};

Tga::Tga(const char* FilePath)
{
    std::fstream hFile(FilePath, std::ios::in | std::ios::binary);
    if (!hFile.is_open()){throw std::invalid_argument("File Not Found.");}

    unsigned char Header[18] = {0};    
    hFile.read(reinterpret_cast<char*>(&Header), sizeof(Header));

    RGB Pixel = {0};
    int CurrentByte = 0;
    size_t CurrentPixel = 0;
    unsigned char ChunkHeader = {0};
    int BytesPerPixel = (BitsPerPixel / 8);
    ImageData.resize(width * height * sizeof(RGB));

    do
    {
        hFile.read(reinterpret_cast<char*>(&ChunkHeader), sizeof(ChunkHeader));

        if(ChunkHeader < 128)
        {
            ++ChunkHeader;
            for(int I = 0; I < ChunkHeader; ++I, ++CurrentPixel)
            {
                hFile.read(reinterpret_cast<char*>(&Pixel), BytesPerPixel);

                ImageData[CurrentByte++] = Pixel.RGBA.B;
                ImageData[CurrentByte++] = Pixel.RGBA.G;
                ImageData[CurrentByte++] = Pixel.RGBA.R;
                if (BitsPerPixel > 24) ImageData[CurrentByte++] = Pixel.RGBA.A;
            }
        }
        else
        {
            ChunkHeader -= 127;
            hFile.read(reinterpret_cast<char*>(&Pixel), BytesPerPixel);

            for(int I = 0; I < ChunkHeader; ++I, ++CurrentPixel)
            {
                ImageData[CurrentByte++] = Pixel.RGBA.B;
                ImageData[CurrentByte++] = Pixel.RGBA.G;
                ImageData[CurrentByte++] = Pixel.RGBA.R;
                if (BitsPerPixel > 24) ImageData[CurrentByte++] = Pixel.RGBA.A;
            }
        }
    } while(CurrentPixel < (width * height));

    hFile.close();
}

void Tga::Save(const char* FilePath)
{
    std::fstream hFile(FilePath, std::ios::out | std::ios::binary);
    if (!hFile.is_open()) {throw std::invalid_argument("Cannot open file for writing.");}

    static unsigned char IsCompressed[12] = {0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0};
    //Need help writing this part.. I wrote out the header and the information needed. I only need to write the pixels compressed. The pixels are stored in: ImageData.

    hFile.write(reinterpret_cast<char*>(&IsCompressed), sizeof(IsCompressed));
    hFile.put((width & 0xFF));
    hFile.put((width & 0xFF) / 0xFF);
    hFile.put((height & 0xFF));
    hFile.put(((height & 0xFF) / 0xFF));
    hFile.put(BitsPerPixel);
    hFile.put(0x0);

    //Write the ImageData here.. Compress it with RLE..
    hFile.close();
}

int main()
{
    //Tga F("C:/Users/Brandon/Desktop/Foo.tga");
    //F.Save("C:/Users/Brandon/Desktop/Boo.tga");
}

RLE: Run-length Encoding. Generally, such encoders can be programmed as an on-the-fly mapping between input (uncompressed image pixels) and output (compressed image pixels). In this case, the encoding is best described in terms of a state machine. There are two states possible when writing pixels:

  1. Writing a sequence of uncompressed pixels (first byte < 127).
  2. Counting a sequence of compressed pixels (first byte > 127).

The state transitions are as follows:

  • Go from State-1 to State-2 whenever two consecutive pixels are the same.
  • Go from State-2 to State-1 whenever the next pixel is different from the "repeated pixel".
  • Renew State-1 whenever the number of written pixels reaches 127 (max).
  • Renew State-2 whenever the number of repeated pixels reaches 127 (max).

With that in mind, you could write the following state classes:

class TGAWriteState {
  public:
    virtual ~TGAWriteState() { };

    virtual bool writePixel(const RGB& pixel) = 0;
    virtual void writeFinished() = 0;
    virtual void init(const RGB& pixel) = 0;
    virtual void finish() = 0;
};

class TGAWriteStateU : public TGAWriteState {
  private:
    RGB lastPixel;
    std::ostream& outFile;
    std::streampos counterPos;
    unsigned char counter;
  public:
    TGAWriteStateU(std::ostream& aOutFile) : lastPixel(), outFile(aOutFile), counterPos(), counter() { };
    virtual ~TGAWriteStateU() {
      if(counterPos)
        finish();
    };

    virtual bool writePixel(const RGB& pixel) {
      if((pixel == lastPixel) && counter)
        return false; // trigger a change of the state.
      if(counter) {
        outFile.put(lastPixel.RGBA.B);
        outFile.put(lastPixel.RGBA.G);
        outFile.put(lastPixel.RGBA.R); // plus alpha channel and stuff...
      };
      ++counter;
      lastPixel = pixel;
      if(counter == 127) { // this means we have reached max, must renew state.
        finish();
        init(pixel);
        writePixel(pixel);
      };
      return true; // keep going with this state.
    };

    // this state always writes one pixel behind to the file, thus, it 
    //  must be notified when the end has been reached, 
    //  so that it can write the last one.
    virtual void writeFinished() {
      outFile.put(lastPixel.RGBA.B);
      outFile.put(lastPixel.RGBA.G);
      outFile.put(lastPixel.RGBA.R); // plus alpha channel and stuff...
      ++counter;
    };

    virtual void init(const RGB& pixel) {
      lastPixel = pixel;
      counter = 0;
      counterPos = outFile.tellp();
      outFile.put(0); // put fake "counter" value.
    };

    virtual void finish() {
      if(counterPos) {
        outFile.seekp(counterPos);
        outFile.put(counter);
        outFile.seekp(0, std::ios_base::end);
        counter = 0;
        counterPos = 0;
      };
    };
};

class TGAWriteStateC : public TGAWriteState {
  private:
    RGB lastPixel;
    std::ostream& outFile;
    unsigned char counter;
  public:
    TGAWriteStateU(std::ostream& aOutFile) : lastPixel(), outFile(aOutFile), counter() { };
    virtual ~TGAWriteStateU() {
      finish();
    };

    virtual bool writePixel(const RGB& pixel) {
      if(pixel != lastPixel)
        return false; // trigger a change of the state.
      ++counter;
      if(counter == 127) { // this means we have reached max, must renew state.
        finish();
        init(pixel);
      };
      return true; // keep going with this state.
    };

    virtual void writeFinished() { };

    virtual void init(const RGB& pixel) {
      lastPixel = pixel;
      counter = 1;
    };

    virtual void finish() {
      if(counter) {
        outFile.put(0x80 | counter);
        outFile.put(lastPixel.RGBA.B);
        outFile.put(lastPixel.RGBA.G);
        outFile.put(lastPixel.RGBA.R); // plus alpha channel and stuff...
        counter = 0;
      };
    };
};

Using the above states, you can roughly write your writing loop as follows:

TGAWriteStateU u_state(hFile);
TGAWriteStateC c_state(hFile);
TGAWriteState* states[2];
states[0] = &u_state; states[1] = &c_state;
states[0]->init(Image[0]);
states[0]->writePixel(Image[0]);

for(int CurrentPixel = 1; CurrentPixel < numberOfPixels; ++CurrentPixel) {
  if( !(states[0]->writePixel(Image[CurrentPixel])) ) {
    states[0]->finish();
    std::swap(states[0], states[1]);
    states[0]->init(Image[CurrentPixel]);
    states[0]->writePixel(Image[CurrentPixel]);
  };
};
states[0]->writeFinished();
states[0]->finish();

There are certainly other ways to do it. You can also achieve roughly the same effect using some state flags and a switch-statement within the loop (not exactly kosher in the C++ world, but OK).

Omg.. Thanks for the reply. I'm going to have to study that code for a while before I get it down. Looking at it scares me ahaha.

Definitely going to sit and study it though before using it as I need to know how to do it. I was reading up on the algorithm yesterday. I understand it a bit more now so hopefully I'll get it fairly soon.

I think the way you explained it. "In two states" and "State Machine" made it much easier to understand than the tutorials elsewhere.

Thanks a lot.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.