Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

.NET Targa Image Reader

0.00/5 (No votes)
15 Dec 2008 1  
Loads a Targa image file into a Bitmap using nothing but .NET code.

Contents

What is TargaImage?

TargaImage was designed to be a simple way to load a TGA image file into a .NET Bitmap object. TargaImage is written entirely in .NET C# code. It does not use any Win32 or other interoperability code, and it does not use any unsafe code blocks. It does not use any third party .NET or unmanaged libraries. If you have the .NET Framework installed, then you can use TargaImage in your code.

TargaImage supports the following commonly used formats:

  • 8, 16, 24, 32 bit pixel depths.
  • 16 and 32 bit alpha channels.
  • 8 bit grayscale images.
  • Indexed and True Color images.
  • Uncompressed and Run-Length Encoded (RLE) compressed image data.

TargaImage does not save images in the Targa format. It was designed for loading Targa images only.

TargaImage does not allow you to convert other image formats into the Targa format. It does, however, allow you to save the loaded image in other formats. See Converting Images for details.

TargaImage was written based on the Truevision TGA Specification 2.0.

TargaImage was written in C# using Visual Studio 2010, and can be used with .NET 2.0 and up.

How to Use TargaImage

TargaImage is very easy to use. After downloading the code, just copy the TargaImage.dll file into your project and add a reference to it. Then, in your code, just call the LoadTargaImage() method. If you need access to the image properties then you'll have to create an instance of the TargaImage class. Just remember to call Dispose() when you're done, as TargaImage does need to release some resources.

Examples

//   C# Sample   
//   Loads a targa image and assigns it to the Image of a picturebox control.
this.PictureBox1.Image = Paloma.TargaImage.LoadTargaImage(@"c:\targaimage.tga");
    
//   Creates an instance of the TargaImage class with the specifed file
//   displays a few targa properties and then assigns the targa image
//   to the Image of a picturebox control
Paloma.TargaImage tgaImage = new Paloma.TargaImage(@"c:\targaimage.tga");
this.Label1.Text = tgaImage.Format.ToString();
this.Label2.Text = tgaImage.Header.ImageType.ToString();
this.Label3.Text = tgaImage.Header.PixelDepth.ToString();
this.PictureBox1.Image = tgaImage.Image; 
tgaImage.Dispose(); // remember to dispose.

 

VB.NET:

'   VB.NET Sample 
'   Loads a targa image and assigns it to the Image of a picturebox control.
Me.PictureBox1.Image = Paloma.TargaImage.LoadTargaImage("c:\targaimage.tga")

    
'   Creates an instance of the TargaImage class with the specifed file
'   displays a few targa properties and then assigns the targa image
'   to the Image of a picturebox control
Dim tgaImage As New Paloma.TargaImage("c:\targaimage.tga")
Me.Label1.Text = tgaImage.Format.ToString()
Me.Label2.Text = tgaImage.Header.ImageType.ToString()
Me.Label3.Text = tgaImage.Header.PixelDepth.ToString()
Me.PictureBox1.Image = tgaImage.Image
tgaImage.Dispose() ' remember to dispose

The Birth of TargaImage

I needed a way to load .tga image files and display them in a PictureBox control. A PictureBox control requires a Bitmap for its Image property. By looking at MSDN, I found that the Bitmap class does not support the .tga image format.

I started to look on the Internet for way to load a .tga file in .NET. I found lots of image libraries available, but all of them used unmanaged code written in C or C++. To use these libraries in my project would require that I use Interop code to get them to work in .NET. There was plenty of Interop code out there, but the code could be used only if the unmanaged libraries were configured correctly. Another problem was that these libraries loaded a .tga file into a Win32 HBITMAP class. This meant I still had to convert the HBitmap object to the Bitmap object I required. I tried to do just that, but the code was cumbersome and messy, and I just didn’t like it. Also, these libraries have many features included with them which seemed to me to be a lot of overhead just to simply load a .tga file.

There was some .NET managed code I found but most of the code could only load very specific types of Targa images, like 32bit true color or 8 bit indexed. This would not work for me because the images I had were of many different types.

I kept looking around for code that could load .tga files, but I just couldn’t find anything out there. I started to think that maybe .NET couldn’t handle loading a .tga file and that was why no one had written code to do it. But that’s just not possible, there has to be a way to read and load .tga files in .NET. So I decided that I would write my own .tga image loader using .NET code only. I will not use any unmanged libraries or interop code. Not only would this solve my problem but maybe it would fill in a small void out there in the .NET community.

The Truevision Targa Specification 2.0

I decided to begin by learning exactly what the Targa image format is. I found the Truevision TGA Specification 2.0 document to be very helpful. This document goes into great detail on how information is saved in a Targa file. If there's anything you wanted to know about the Targa format, it's in this document.

Here is the structure of a .tga image file. The picture is from the specification document.

Targa image file structure

Based on the description of the Targa image file structure in this document, I developed TargaImage with the following classes:

  • TargaImage
  • TargaHeader
  • TargaExtensionArea
  • TargaFooter

TargaImage

This is the main class and it holds the TargaHeader, TargaExtensionArea, and TargaFooter. The Image Data section is loaded into the Image property of this class.

TargaHeader

This class holds all of the header properties of a Targa image. This includes the TGA File Header, Image ID, and Color Map section.

TargaExtensionArea

This class holds all of the Extension Area properties of the Targa image, if any exist in the file.

TargaFooter

This class holds all of the TGA File Footer properties of the Targa image, if any exist in the file.

Note: The Developer Area section is ignored by TargaImage because this section was designed for custom use by developers, and will only have significance if you know what to do with the data stored in this area. TargaImage does provide access to the DeveloperDirectoryOffset value if one is found within the file.

How it all Works

Loading the File

Because of how the data is structured in the file, we will require the ability to move around to different areas of the file. To do this, we will first load the bytes of the file into memory and then create a BinaryReader based off the bytes. The BinaryReader makes it easy to read a few bytes at a time, while at the same time allowing us to move to different areas of the file.

Following the Targa specification, the first step is to check for and load the Footer section. If a Footer exists, we will need to load it first, because it holds the offset values to other sections within the file, mainly the Extension Area section. After loading the Footer, we can load the Header section and then the Extension Area section. Once all of the sections are loaded, we have all the information we need to properly load the Image Data.

Note: The error checking code was left out for brevity.

// load the file as an array of bytes
filebytes = System.IO.File.ReadAllBytes(this.strFileName);
// create a seekable memory stream of the file bytes
using (filestream = new MemoryStream(filebytes))
{
   // create a BinaryReader used to read the Targa file
   using (binReader = new BinaryReader(filestream))
   {
      // Load each section and the image data itself
      this.LoadTGAFooterInfo(binReader);
      this.LoadTGAHeaderInfo(binReader);
      this.LoadTGAExtensionArea(binReader);
      this.LoadTGAImage(binReader);
   }
}

Loading Image Data into a Bitmap

Before loading the image data, we have to compute the values needed by the Bitmap class so that it can display the image data properly. According to the MSDN documentation, there are five values we need to have to be able to create a Bitmap object using our image data.

  1. The width of the image in pixels.
  2. The height of the image in pixels.
  3. The stride of the image in bytes.
  4. The pixel format of the image. E.g., 32 bit ARGB, 8 bit indexed, etc.
  5. A pointer to an array of bytes that contains the pixel data.

For the width and height, we will use the Width and the Height property of the TargaHeader class.

Now, stride is a bit trickier. In a Bitmap object, stride refers to the length in bytes of each row, or scan line, in an image, and must be aligned on a 32 bit boundary or 4 bytes. What this means is that we need to make sure that the stride value is the closet multiple of 4 that is greater than the width in bytes.

For example, if our image pixel width is 23 pixels and our pixel depth is 16 bits (2 bytes), then the width of our image in bytes is 23 * 2 = 46. Our closest multiple of 4 greater than 46 is 48, so our stride for this image would be 48.

Here is the code that calculates the stride of an image. This code uses bitwise logic and bit shifting to accomplish the calculation.

// calculate the stride, in bytes, of the image (32bit aligned width of each image row)
this.intStride = (((int)this.objTargaHeader.Width * 
                 (int)this.objTargaHeader.PixelDepth + 31) & ~31) >> 3;

The pixel format has to be one of the PixelFormat enumeration values. To determine the PixelFormat, we use the TargaImage.Format, TargaHeader.PixelDepth, TargaHeader.AttributeBits and TargaExtensionArea.AttributesType properties.

/// <summary>
/// Gets the PixelFormat to be used by the Image based on the Targa file's attributes
/// </summary>
/// <returns></returns>
private PixelFormat GetPixelFormat()
{
    PixelFormat pfTargaPixelFormat = PixelFormat.Undefined;
    // first off what is our Pixel Depth (bits per pixel)
    switch (this.objTargaHeader.PixelDepth)
    {
        case 8:
            pfTargaPixelFormat = PixelFormat.Format8bppIndexed;
            break;

        case 16:
            // if this is a new tga file and we have an extension area, we'll determine the alpha based on
            // the extension area Attributes
            if (this.Format == TGAFormat.NEW_TGA && this.objTargaFooter.ExtensionAreaOffset > 0)
            {
                switch (this.objTargaExtensionArea.AttributesType)
                {
                    case 0:
                    case 1:
                    case 2: // no alpha data
                        pfTargaPixelFormat = PixelFormat.Format16bppRgb555;
                        break;

                    case 3: // useful alpha data
                        pfTargaPixelFormat = PixelFormat.Format16bppArgb1555;
                        break;
                }
            }
            else
            {
                // just a regular tga, determine the alpha based on the Header Attributes
                if (this.Header.AttributeBits == 0)
                    pfTargaPixelFormat = PixelFormat.Format16bppRgb555;
                if (this.Header.AttributeBits == 1)
                    pfTargaPixelFormat = PixelFormat.Format16bppArgb1555;
            }

            break;

        case 24:
            pfTargaPixelFormat = PixelFormat.Format24bppRgb;
            break;

        case 32:
            // if this is a new tga file and we have an extension area,
            // we'll determine the alpha based on
            // the extension area Attributes
            if (this.Format == TGAFormat.NEW_TGA && this.objTargaFooter.ExtensionAreaOffset > 0)
            {
                switch (this.objTargaExtensionArea.AttributesType)
                {

                    case 0:
                    case 1:
                    case 2: // no alpha data
                        pfTargaPixelFormat = PixelFormat.Format32bppRgb;
                        break;

                    case 3: // useful alpha data
                        pfTargaPixelFormat = PixelFormat.Format32bppArgb;
                        break;

                    case 4: // premultiplied alpha data
                        pfTargaPixelFormat = PixelFormat.Format32bppPArgb;
                        break;
                }
            }
            else
            {
                // just a regular tga, determine the alpha based on the Header Attributes
                if (this.Header.AttributeBits == 0)
                    pfTargaPixelFormat = PixelFormat.Format32bppRgb;
                if (this.Header.AttributeBits == 8)
                    pfTargaPixelFormat = PixelFormat.Format32bppArgb;
                break;
            }
            break;
    }
    return pfTargaPixelFormat;
}

 

Now, we have to get a pointer to the array of bytes that holds the pixel data. To do this, we will load the pixel data into a byte array, and then using a GCHandle class, we can "pin" the memory address of the array so that the garbage collector will not move or delete the memory. We can also use the GCHandle class to get a pointer to the "pinned" memory by calling the AddrOfPinnedObject() method. This will return a memory pointer as an IntPtr value. IntPtrs are the way .NET represents a pointer to a memory address.

See Loading the Image Data to see how the image data is loaded into the byte array:

// get the image data bytes
byte[] bimagedata = this.LoadImageBytes(binReader);

// since the Bitmap constructor requires a pointer to an array of image bytes
// we have to pin down the memory used by the byte array and use the pointer 
// of this pinned memory to create the Bitmap.
// This tells the Garbage Collector to leave the memory alone and DO NOT touch it.
this.ImageByteHandle = GCHandle.Alloc(bimagedata, GCHandleType.Pinned);

Now that we have the width, height, stride, pixel format, and image data byte array ready to go, we can create a Bitmap object.

// create a Bitmap object using the image Width, Height,
// Stride, PixelFormat and the pointer to the pinned byte array.
this.bmpTargaImage = new Bitmap((int)this.objTargaHeader.Width,
                                (int)this.objTargaHeader.Height,
                                     this.intStride,
                                     pfPixelFormat,
                                     this.ImageByteHandle.AddrOfPinnedObject());

Loading the Image Data

The goal of loading the image data is to get the bytes of the data into a byte array that we can then use to create the Bitmap object. TargaImage supports both Run-Length Encoding (RLE) compressed and uncompressed image data. To determine if an image is RLE compressed or uncompressed, you can check the TargaHeader.ImageType property.

If the image data is stored as uncompressed bytes, we can load them in the order they are stored in the file.

for (int i = 0; i < (int)this.objTargaHeader.Height; i++)
{
    for (int j = 0; j < intImageRowByteSize; j++)
    {
       row.Add(binReader.ReadByte());
    }
    rows.Add(row);
    row = new System.Collections.Generic.List<byte>();
}

When an image has been compressed using RLE, you will have to decode the RLE compression. RLE is a simple compression that encodes runs of identical pixels into a single packet.

There are two types of RLE packets, Run-Length and Raw packets. A Run-Length packet has a pixel value and the number of times this pixel value has to be repeated. A Raw packet has a pixel count and then the pixel data itself.

To load the pixel data, you first have to determine which type of RLE packet you have, then for a Run-Length packet, repeat the pixel value the specified number of times, or with a Raw packet, read the number of pixels specified. See the Targa Specification 2.0 document for more details on how RLE works.

Here is the code that decodes each type of RLE packet:

// check the RLE packet type
if ((RLEPacketType)intRLEPacketType == RLEPacketType.RUN_LENGTH)
{
   // get the pixel color data
   bRunLengthPixel = binReader.ReadBytes((int)this.objTargaHeader.BytesPerPixel);
   // add the number of pixels specified using the read pixel color
   for (int i = 0; i < intRLEPixelCount; i++)
   {
      foreach (byte b in bRunLengthPixel)
         row.Add(b);
    
      // increment the byte counts
      intImageRowBytesRead += bRunLengthPixel.Length;
      intImageBytesRead += bRunLengthPixel.Length;

      // if we have read a full image row
      // add the row to the row list and clear it
      // restart row byte count
      if (intImageRowBytesRead == intImageRowByteSize)
      {
         rows.Add(row);
         row = new System.Collections.Generic.List<byte>();
         intImageRowBytesRead = 0;
      }
   }
}
else if ((RLEPacketType)intRLEPacketType == RLEPacketType.RAW)
{
   // get the number of bytes to read based on the read pixel count
   int intBytesToRead = intRLEPixelCount * (int)this.objTargaHeader.BytesPerPixel;

   // read each byte
   for (int i = 0;i < intBytesToRead;i++)
   {
      row.Add(binReader.ReadByte());

      // increment the byte counts
      intImageBytesRead++;
      intImageRowBytesRead++;

      // if we have read a full image row
      // add the row to the row list and clear it
      // restart row byte count
      if (intImageRowBytesRead == intImageRowByteSize)
      {
         rows.Add(row);
         row = new System.Collections.Generic.List<byte>();
                                            intImageRowBytesRead = 0;
      }
   }
}

We must also take into account the ordering in which the pixels were saved. We need to check the objTargaHeader.FirstPixelDestination property, and based on its value, return the bytes in the proper order.

// use FirstPixelDestination to determine the alignment of the 
// image data byte
switch (this.objTargaHeader.FirstPixelDestination)
{
   case FirstPixelDestination.TOP_LEFT:
      blnRowsReverse = false;
      blnEachRowReverse = true;
      break;

   case FirstPixelDestination.TOP_RIGHT:
      blnRowsReverse = false;
      blnEachRowReverse = false;
      break;

   case FirstPixelDestination.BOTTOM_LEFT:
      blnRowsReverse = true;
      blnEachRowReverse = true;
      break;

   case FirstPixelDestination.BOTTOM_RIGHT:
   case FirstPixelDestination.UNKNOWN:
      blnRowsReverse = true;
      blnEachRowReverse = false;
      break;
}

// write the bytes from each row into a memory stream and get the 
// resulting byte array
using (msData = new MemoryStream())
{
   // do we reverse the rows in the row list.
   if (blnRowsReverse == true)
      rows.Reverse();

   // go through each row
   for (int i = 0; i < rows.Count; i++)
   {
      // do we reverse the bytes in the row
      if (blnEachRowReverse == true)
         rows[i].Reverse();

      // get the byte array for the row
      byte[] brow = rows[i].ToArray();

      // write the row bytes and padding bytes to the memory streem
      msData.Write(brow, 0, brow.Length);
      msData.Write(padding, 0, padding.Length);
   }
      
   // get the image byte array
   data = msData.ToArray(); 
                        
}

Converting Images

Since TargaImage loads the image into a Bitmap class, you can use TargaImage to convert images from Targa to any format supported by Bitmap.

//   C# Sample
//   Load a targa image and save it in various image formats.
Bitmap tga = Paloma.TargaImage.LoadTargaImage(@"c:\targaimage.tga");

tga.Save(@"c:\targaimage.jpg");
tga.Save(@"c:\targaimage.gif");
tga.Save(@"c:\targaimage.png");
tga.Save(@"c:\targaimage.bmp");

VB.NET:

'   VB.NET Sample 
'   Load a targa image and save it in various image formats.
Dim tga As Bitmap = Paloma.TargaImage.LoadTargaImage("c:\targaimage.tga")
    
tga.Save("c:\targaimage.jpg")
tga.Save("c:\targaimage.gif")
tga.Save("c:\targaimage.png")
tga.Save("c:\targaimage.bmp")

TargaImage Demo Application

To see TargaImage in action, you can download the demo application. It comes with several sample .tga images that you can load to view and inspect their properties.

If you have a .tga file of your own, you can use the “Load” button to load it using TargaImage.

If TargaImage cannot load your .tga file, please send me a copy of the image so I can implement any fixes, or if you fix the code yourself, send me the changes you made. Any contributions will be a great help in keeping TargaImage as versatile as possible.

Conclusion

Working on TargaImage has taught me that the .NET framework puts a vast library of tools at the programmer's disposal, and problems can be solved without relying on Win32 or unmanaged code. I also learned that with those tools, it is possible to do low-level byte and bit programming in .NET without resorting to unmanaged code.

I hope you find that TargaImage makes it easy to load and display Targa images in your own .NET projects. I have been using TargaImage in a couple of projects, and it has been performing beautifully.

Helpful References

History

  • 12/15/2008 - Release of TargaImage 1.0.
  • 10/24/2015 - Release of TargaImage 1.1
    • Updated the GetPixelFormat() method to correctly determine the 16 and 32 bit alpha channels.
    • Added the ability to load an image using a Stream.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here