65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET Targa 图像读取器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (56投票s)

2008年12月16日

CPOL

10分钟阅读

viewsIcon

146825

downloadIcon

9472

使用纯 .NET 代码将 Targa 图像文件加载到 Bitmap 对象中。

目录

什么是 TargaImage?

TargaImage 被设计成一种简单的方式,可以将 TGA 图像文件加载到 .NET 的 Bitmap 对象中。TargaImage 完全使用 .NET C# 代码编写。它不使用任何 Win32 或其他互操作代码,也不使用任何 unsafe 代码块。它不使用任何第三方 .NET 或非托管库。如果您安装了 .NET Framework,那么就可以在您的代码中使用 TargaImage。

TargaImage 支持以下常用格式

  • 8、16、24、32 位像素深度。
  • 16 和 32 位 Alpha 通道。
  • 8 位灰度图像。
  • 索引和真彩色图像。
  • 未压缩和行程长度编码 (RLE) 压缩的图像数据。

TargaImage 不以 Targa 格式保存图像。它仅设计用于加载 Targa 图像。

TargaImage 不允许您将其他图像格式转换为 Targa 格式。但是,它允许您将加载的图像保存为其他格式。有关详细信息,请参阅转换图像

TargaImage 是根据 Truevision TGA 规范 2.0 编写的。

TargaImage 使用 Visual Studio 2010 以 C# 编写,可与 .NET 2.0 及更高版本一起使用。

如何使用 TargaImage

TargaImage 非常易于使用。下载代码后,只需将 TargaImage.dll 文件复制到您的项目中并添加对它的引用。然后,在您的代码中,只需调用 LoadTargaImage() 方法即可。如果您需要访问图像属性,则必须创建一个 TargaImage 类的实例。请记住,完成后调用 Dispose(),因为 TargaImage 确实需要释放一些资源。

示例

//   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

TargaImage 的诞生

我需要一种方法来加载 .tga 图像文件并在 PictureBox 控件中显示它们。PictureBox 控件需要一个 Bitmap 来设置其 Image 属性。通过查看 MSDN,我发现 Bitmap 类不支持 .tga 图像格式。

我开始在网上查找在 .NET 中加载 .tga 文件的方法。我找到了许多可用的图像库,但它们都使用了用 C 或 C++ 编写的非托管代码。要在我的项目中使用这些库,我需要使用 Interop 代码才能在 .NET 中使用它们。那里有很多 Interop 代码,但只有在正确配置了非托管库时才能使用这些代码。另一个问题是,这些库将 .tga 文件加载到 Win32 HBITMAP 类中。这意味着我仍然需要将 HBITMAP 对象转换为我需要的 Bitmap 对象。我尝试这样做,但代码很麻烦而且混乱,我就是不喜欢它。此外,这些库包含了许多我认为只是为了简单加载 .tga 文件而引入的许多功能,这带来了很多开销。

我找到了一些 .NET 托管代码,但大部分代码只能加载非常特定的 Targa 图像类型,如 32 位真彩色或 8 位索引。这对我来说不行,因为我拥有的图像类型很多。

我继续寻找可以加载 .tga 文件的代码,但就是找不到。我开始认为也许 .NET 无法处理加载 .tga 文件,这就是为什么没有人编写代码来做这件事。但这根本不可能,一定有办法在 .NET 中读取和加载 .tga 文件。所以我决定用纯 .NET 代码编写自己的 .tga 图像加载器。我不会使用任何非托管库或互操作代码。这不仅能解决我的问题,也许还能填补 .NET 社区中一个小小的空白。

Truevision Targa 规范 2.0

我决定首先确切地了解 Targa 图像格式。我发现 Truevision TGA 规范 2.0 文档非常有帮助。该文档详细介绍了 Targa 文件中的信息是如何保存的。如果您想了解 Targa 格式的任何信息,都可以在此文档中找到。

下面是 .tga 图像文件的结构。图片来自规范文档。

Targa image file structure

基于本文档中对 Targa 图像文件结构的描述,我开发了 TargaImage,包含以下类

  • TargaImage
  • TargaHeader
  • TargaExtensionArea
  • TargaFooter

TargaImage

这是主类,它包含 TargaHeaderTargaExtensionAreaTargaFooter。图像数据部分加载到此类的 Image 属性中。

TargaHeader

此类包含 Targa 图像的所有头属性。这包括 TGA 文件头、图像 ID 和颜色映射部分。

TargaExtensionArea

此类包含 Targa 图像的所有扩展区域属性,如果文件中存在的话。

TargaFooter

此类包含 Targa 图像的所有 TGA 文件尾属性,如果文件中存在的话。

注意: TargaImage 忽略开发者区域,因为该区域是为开发者自定义用途而设计的,只有了解该区域存储数据的含义时才具有意义。如果文件中发现了 DeveloperDirectoryOffset 值,TargaImage 确实提供了访问它的途径。

工作原理

加载文件

由于文件中的数据结构,我们需要能够移动到文件的不同区域。为此,我们将首先将文件的字节加载到内存中,然后基于这些字节创建一个 BinaryReaderBinaryReader 可以轻松地一次读取几个字节,同时允许我们移动到文件的不同区域。

根据 Targa 规范,第一步是检查并加载尾部部分。如果存在尾部,我们必须首先加载它,因为它包含文件中其他部分的偏移量值,主要是扩展区域部分。加载尾部后,我们可以加载头部部分,然后加载扩展区域部分。一旦所有部分都加载完成,我们就拥有了正确加载图像数据所需的所有信息。

注意:为简洁起见,省略了错误检查代码。

// 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);
   }
}

将图像数据加载到 Bitmap 中

在加载图像数据之前,我们必须计算 Bitmap 类所需的数值,以便它能够正确显示图像数据。根据 MSDN 文档,我们需要五个值才能使用我们的图像数据创建一个 Bitmap 对象。

  1. 图像的宽度(以像素为单位)。
  2. 图像的高度(以像素为单位)。
  3. 图像的跨度(以字节为单位)。
  4. 图像的像素格式。例如,32 位 ARGB、8 位索引等。
  5. 指向包含像素数据的字节数组的指针。

对于宽度和高度,我们将使用 TargaHeader 类的 WidthHeight 属性。

现在,跨度有点棘手。在 Bitmap 对象中,跨度是指图像中每行(或扫描线)的字节长度,并且必须对齐在 32 位边界或 4 字节上。这意味着我们需要确保跨度值是大于以字节为单位的宽度的最接近的 4 的倍数。

例如,如果我们的图像像素宽度为 23 像素,像素深度为 16 位(2 字节),那么我们的图像宽度(以字节为单位)为 23 * 2 = 46。大于 46 的最接近的 4 的倍数是 48,所以这个图像的跨度将是 48。

这是计算图像跨度的代码。这段代码使用位逻辑和位移来完成计算。

// 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;

像素格式必须是 PixelFormat 枚举值之一。为了确定 PixelFormat,我们使用 TargaImage.FormatTargaHeader.PixelDepthTargaHeader.AttributeBitsTargaExtensionArea.AttributesType 属性。

/// <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;
}

 

现在,我们必须获取包含像素数据的字节数组的指针。为了做到这一点,我们将像素数据加载到一个字节数组中,然后使用 GCHandle 类,我们可以“固定”该数组的内存地址,这样垃圾回收器就不会移动或删除内存。我们还可以通过调用 AddrOfPinnedObject() 方法来使用 GCHandle 类获取“固定”内存的指针。这将返回一个作为 IntPtr 值的内存指针。IntPtr 是 .NET 表示内存地址指针的方式。

请参阅加载图像数据,了解图像数据如何加载到字节数组中。

// 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);

现在我们已经准备好了宽度、高度、跨度、像素格式和图像数据字节数组,我们可以创建一个 Bitmap 对象。

// 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());

加载图像数据

加载图像数据的目标是将数据字节放入一个字节数组,然后我们可以使用该数组创建 Bitmap 对象。TargaImage 支持行程长度编码 (RLE) 压缩和未压缩的图像数据。要确定图像是 RLE 压缩还是未压缩,可以检查 TargaHeader.ImageType 属性。

如果图像数据存储为未压缩字节,我们可以按照它们在文件中的存储顺序加载它们。

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>();
}

当图像使用 RLE 压缩时,您必须解码 RLE 压缩。RLE 是一种简单的压缩,它将相同的像素运行编码为单个数据包。

有两种类型的 RLE 数据包:行程长度数据包和原始数据包。行程长度数据包包含一个像素值以及该像素值必须重复的次数。原始数据包包含像素计数,然后是像素数据本身。

要加载像素数据,您首先必须确定您拥有哪种类型的 RLE 数据包,然后对于行程长度数据包,将像素值重复指定的次数,或者对于原始数据包,读取指定的像素数量。有关 RLE 工作原理的更多详细信息,请参阅 Targa 规范 2.0 文档。

这是解码每种 RLE 数据包的代码

// 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;
      }
   }
}

我们还必须考虑像素的保存顺序。我们需要检查 objTargaHeader.FirstPixelDestination 属性,并根据其值,以正确的顺序返回字节。

// 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(); 
                        
}

转换图像

由于 TargaImage 将图像加载到 Bitmap 类中,因此您可以使用 TargaImage 将图像从 Targa 转换为 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 演示应用程序

要查看 TargaImage 的实际效果,您可以下载演示应用程序。它附带了几张示例 .tga 图像,您可以加载这些图像来查看和检查它们的属性。

如果您有自己的 .tga 文件,您可以使用“加载”按钮使用 TargaImage 来加载它。

如果 TargaImage 无法加载您的 .tga 文件,请将图像的副本发送给我,以便我可以进行修复,或者如果您自己修复了代码,请将您所做的更改发送给我。任何贡献都将极大地有助于使 TargaImage 尽可能通用。

结论

通过处理 TargaImage,我了解到 .NET 框架为程序员提供了海量的工具库,并且可以在不依赖 Win32 或非托管代码的情况下解决问题。我还了解到,利用这些工具,可以在 .NET 中进行低级的字节和位编程,而无需诉诸非托管代码。

我希望您发现 TargaImage 可以轻松地在您自己的 .NET 项目中加载和显示 Targa 图像。我已经在几个项目中使用了 TargaImage,并且它的性能一直很好。

有用参考

历史

  • 2008年12月15日 - 发布 TargaImage 1.0。
  • 2015年10月24日 - 发布 TargaImage 1.1
    • 更新了 GetPixelFormat() 方法,以正确确定 16 位和 32 位 Alpha 通道。
    • 增加了使用 Stream 加载图像的功能。
© . All rights reserved.