Contents
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.
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
this.PictureBox1.Image = Paloma.TargaImage.LoadTargaImage(@"c:\targaimage.tga");
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();
VB.NET:
Me.PictureBox1.Image = Paloma.TargaImage.LoadTargaImage("c:\targaimage.tga")
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()
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.
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.
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.
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.
filebytes = System.IO.File.ReadAllBytes(this.strFileName);
using (filestream = new MemoryStream(filebytes))
{
using (binReader = new BinaryReader(filestream))
{
this.LoadTGAFooterInfo(binReader);
this.LoadTGAHeaderInfo(binReader);
this.LoadTGAExtensionArea(binReader);
this.LoadTGAImage(binReader);
}
}
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.
- The width of the image in pixels.
- The height of the image in pixels.
- The stride of the image in bytes.
- The pixel format of the image. E.g., 32 bit ARGB, 8 bit indexed, etc.
- 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.
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.
private PixelFormat GetPixelFormat()
{
PixelFormat pfTargaPixelFormat = PixelFormat.Undefined;
switch (this.objTargaHeader.PixelDepth)
{
case 8:
pfTargaPixelFormat = PixelFormat.Format8bppIndexed;
break;
case 16:
if (this.Format == TGAFormat.NEW_TGA && this.objTargaFooter.ExtensionAreaOffset > 0)
{
switch (this.objTargaExtensionArea.AttributesType)
{
case 0:
case 1:
case 2: pfTargaPixelFormat = PixelFormat.Format16bppRgb555;
break;
case 3: pfTargaPixelFormat = PixelFormat.Format16bppArgb1555;
break;
}
}
else
{
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.Format == TGAFormat.NEW_TGA && this.objTargaFooter.ExtensionAreaOffset > 0)
{
switch (this.objTargaExtensionArea.AttributesType)
{
case 0:
case 1:
case 2: pfTargaPixelFormat = PixelFormat.Format32bppRgb;
break;
case 3: pfTargaPixelFormat = PixelFormat.Format32bppArgb;
break;
case 4: pfTargaPixelFormat = PixelFormat.Format32bppPArgb;
break;
}
}
else
{
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. IntPtr
s 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:
byte[] bimagedata = this.LoadImageBytes(binReader);
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.
this.bmpTargaImage = new Bitmap((int)this.objTargaHeader.Width,
(int)this.objTargaHeader.Height,
this.intStride,
pfPixelFormat,
this.ImageByteHandle.AddrOfPinnedObject());
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:
if ((RLEPacketType)intRLEPacketType == RLEPacketType.RUN_LENGTH)
{
bRunLengthPixel = binReader.ReadBytes((int)this.objTargaHeader.BytesPerPixel);
for (int i = 0; i < intRLEPixelCount; i++)
{
foreach (byte b in bRunLengthPixel)
row.Add(b);
intImageRowBytesRead += bRunLengthPixel.Length;
intImageBytesRead += bRunLengthPixel.Length;
if (intImageRowBytesRead == intImageRowByteSize)
{
rows.Add(row);
row = new System.Collections.Generic.List<byte>();
intImageRowBytesRead = 0;
}
}
}
else if ((RLEPacketType)intRLEPacketType == RLEPacketType.RAW)
{
int intBytesToRead = intRLEPixelCount * (int)this.objTargaHeader.BytesPerPixel;
for (int i = 0;i < intBytesToRead;i++)
{
row.Add(binReader.ReadByte());
intImageBytesRead++;
intImageRowBytesRead++;
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.
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;
}
using (msData = new MemoryStream())
{
if (blnRowsReverse == true)
rows.Reverse();
for (int i = 0; i < rows.Count; i++)
{
if (blnEachRowReverse == true)
rows[i].Reverse();
byte[] brow = rows[i].ToArray();
msData.Write(brow, 0, brow.Length);
msData.Write(padding, 0, padding.Length);
}
data = msData.ToArray();
}
Since TargaImage
loads the image into a Bitmap
class, you can use TargaImage to convert images from Targa to any format supported by Bitmap
.
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:
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")
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.
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.
- 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.