C#-正确加载索引的彩色图像文件 [英] C# - Loading an indexed color image file correctly

查看:73
本文介绍了C#-正确加载索引的彩色图像文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我创建了一个索引颜色,即每像素8位PNG(我已经用ImageMagick检查了格式是否正确),我想在保持8bpp像素格式的同时将其从磁盘加载到System.Drawing.Bitmap中.以便查看(和操作)其调色板.但是,如果我创建这样的位图:

So I created an indexed color, 8 bits-per-pixel PNG (I already checked with ImageMagick if the format is correct) and I want to load it from disk into a System.Drawing.Bitmap while keeping the 8bpp pixel format, in order to view (and manipulate) its palette. However, if I create a Bitmap like this:

Bitmap bitmap = new Bitmap("indexed-image.png");

生成的位图将自动转换为32bpp图像格式,并且bitmap.Palette.Entries字段显示为空.

The resulting Bitmap gets automatically converted to a 32bpp image format, and the bitmap.Palette.Entries field comes out as empty.

如何在C#中将32bpp图像转换为8bpp"的问题的答案?在StackOverflow上说,这可能是将其转换回8bpp的有效方法:

An answer to the question "How to convert a 32bpp image to 8bpp in C#?" here on StackOverflow said that this could be a valid way to convert it back to 8bpp:

bitmap = bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), PixelFormat.Format8bppIndexed);

但是,这会产生不正确的结果,因为调色板中的某些颜色完全是错误的.

This, however, produces incorrect results, as some colors in the palette are just plain wrong.

如何将图像本地加载为8bpp,或者至少正确地将32bpp的图像转换为8bpp?

How can I load an image natively to 8bpp, or at least correctly convert a 32bpp one to 8bpp?

推荐答案

我也遇到了这个问题,看来任何包含透明度的调色板png图像 都无法加载,因为.NET框架,尽管.Net函数可以完美地 write 这样的文件.相反,如果文件为gif格式,或者调色板的png具有 no 透明性,则没有任何问题.

I had this problem too, and it seems that any paletted png image that contains transparency can't be loaded as being paletted by the .Net framework, despite the fact the .Net functions can perfectly write such a file. In contrast, it has no problems with this if the file is in gif format, or if the paletted png has no transparency.

调色板png中的透明度通过在标题中添加可选的"tRNS"块来指定每个调色板条目的alpha来工作. .Net类正确读取并应用了该类,因此我真的不明白为什么他们随后坚持将图像转换为32位.

Transparency in paletted png works by adding an optional "tRNS" chunk in the header, to specify the alpha of each palette entry. The .Net classes read and apply this correctly, so I don't really understand why then they insist on converting the image to 32 bit afterwards.

png格式的结构非常简单;在标识字节之后,每个块是内容大小的4个字节(大端),然后是4个ASCII字符作为块ID,然后是块内容本身,最后是一个4字节的块CRC值(再次,保存为大字节) -endian).

The structure of the png format is fairly simple; after the identifying bytes, each chunk is 4 bytes of the content size (big-endian), then 4 ASCII characters for the chunk id, then the chunk content itself, and finally a 4-byte chunk CRC value (again, saved as big-endian).

鉴于这种结构,解决方案非常简单:

Given this structure, the solution is fairly simple:

  • 将文件读入字节数组.
  • 通过分析标题,确保它是调色板的png文件.
  • 通过从块头跳转到块头来找到"tRNS"块.
  • 从块中读取alpha值.
  • 制作一个新的字节数组,其中包含图像数据,但切出了"tRNS"块.
  • 使用从调整后的字节数据创建的MemoryStream创建Bitmap对象,从而生成正确的8位图像.
  • 使用提取的alpha数据修复调色板.
  • Read the file into a byte array.
  • Ensure it is a paletted png file by analysing the header.
  • Find the "tRNS" chunk by jumping from chunk header to chunk header.
  • Read the alpha values from the chunk.
  • Make a new byte array containing the image data, but with the "tRNS" chunk cut out.
  • Create the Bitmap object using a MemoryStream created from the adjusted byte data, resulting in the correct 8-bit image.
  • Fix the color palette using the extracted alpha data.

如果您正确地执行检查和后备操作,则可以使用此功能加载任何图像,并且如果该图像恰好被标识为具有透明性信息的调色板png,它将执行修复.

If you do the checks and fallbacks right you can just load any image with this function, and if it happens to identify as paletted png with transparency info it'll perform the fix.

/// <summary>
/// Image loading toolset class which corrects the bug that prevents paletted PNG images with transparency from being loaded as paletted.
/// </summary>
public class BitmapHandler
{

    private static Byte[] PNG_IDENTIFIER = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};

    /// <summary>
    /// Loads an image, checks if it is a PNG containing palette transparency, and if so, ensures it loads correctly.
    /// The theory on the png internals can be found at http://www.libpng.org/pub/png/book/chapter08.html
    /// </summary>
    /// <param name="data">File data to load.</param>
    /// <returns>The loaded image.</returns>
    public static Bitmap LoadBitmap(Byte[] data)
    {
        Byte[] transparencyData = null;
        if (data.Length > PNG_IDENTIFIER.Length)
        {
            // Check if the image is a PNG.
            Byte[] compareData = new Byte[PNG_IDENTIFIER.Length];
            Array.Copy(data, compareData, PNG_IDENTIFIER.Length);
            if (PNG_IDENTIFIER.SequenceEqual(compareData))
            {
                // Check if it contains a palette.
                // I'm sure it can be looked up in the header somehow, but meh.
                Int32 plteOffset = FindChunk(data, "PLTE");
                if (plteOffset != -1)
                {
                    // Check if it contains a palette transparency chunk.
                    Int32 trnsOffset = FindChunk(data, "tRNS");
                    if (trnsOffset != -1)
                    {
                        // Get chunk
                        Int32 trnsLength = GetChunkDataLength(data, trnsOffset);
                        transparencyData = new Byte[trnsLength];
                        Array.Copy(data, trnsOffset + 8, transparencyData, 0, trnsLength);
                        // filter out the palette alpha chunk, make new data array
                        Byte[] data2 = new Byte[data.Length - (trnsLength + 12)];
                        Array.Copy(data, 0, data2, 0, trnsOffset);
                        Int32 trnsEnd = trnsOffset + trnsLength + 12;
                        Array.Copy(data, trnsEnd, data2, trnsOffset, data.Length - trnsEnd);
                        data = data2;
                    }
                }
            }
        }
        using(MemoryStream ms = new MemoryStream(data))
        using(Bitmap loadedImage = new Bitmap(ms))
        {
            if (loadedImage.Palette.Entries.Length != 0 && transparencyData != null)
            {
                ColorPalette pal = loadedImage.Palette;
                for (int i = 0; i < pal.Entries.Length; i++)
                {
                    if (i >= transparencyData.Length)
                        break;
                    Color col = pal.Entries[i];
                    pal.Entries[i] = Color.FromArgb(transparencyData[i], col.R, col.G, col.B);
                }
                loadedImage.Palette = pal;
            }
            // Images in .Net often cause odd crashes when their backing resource disappears.
            // This prevents that from happening by copying its inner contents into a new Bitmap object.
            return CloneImage(loadedImage, null);
        }
    }

    /// <summary>
    /// Finds the start of a png chunk. This assumes the image is already identified as PNG.
    /// It does not go over the first 8 bytes, but starts at the start of the header chunk.
    /// </summary>
    /// <param name="data">The bytes of the png image.</param>
    /// <param name="chunkName">The name of the chunk to find.</param>
    /// <returns>The index of the start of the png chunk, or -1 if the chunk was not found.</returns>
    private static Int32 FindChunk(Byte[] data, String chunkName)
    {
        if (data == null)
            throw new ArgumentNullException("data", "No data given!");
        if (chunkName == null)
            throw new ArgumentNullException("chunkName", "No chunk name given!");
        // Using UTF-8 as extra check to make sure the name does not contain > 127 values.
        Byte[] chunkNamebytes = Encoding.UTF8.GetBytes(chunkName);
        if (chunkName.Length != 4 || chunkNamebytes.Length != 4)
            throw new ArgumentException("Chunk name must be 4 ASCII characters!", "chunkName");
        Int32 offset = PNG_IDENTIFIER.Length;
        Int32 end = data.Length;
        Byte[] testBytes = new Byte[4];
        // continue until either the end is reached, or there is not enough space behind it for reading a new chunk
        while (offset + 12 < end)
        {
            Array.Copy(data, offset + 4, testBytes, 0, 4);
            if (chunkNamebytes.SequenceEqual(testBytes))
                return offset;
            Int32 chunkLength = GetChunkDataLength(data, offset);
            // chunk size + chunk header + chunk checksum = 12 bytes.
            offset += 12 + chunkLength;
        }
        return -1;
    }

    private static Int32 GetChunkDataLength(Byte[] data, Int32 offset)
    {
        if (offset + 4 > data.Length)
            throw new IndexOutOfRangeException("Bad chunk size in png image.");
        // Don't want to use BitConverter; then you have to check platform endianness and all that mess.
        Int32 length = data[offset + 3] + (data[offset + 2] << 8) + (data[offset + 1] << 16) + (data[offset] << 24);
        if (length < 0)
            throw new IndexOutOfRangeException("Bad chunk size in png image.");
        return length;
    }

    /// <summary>
    /// Clones an image object to free it from any backing resources.
    /// Code taken from http://stackoverflow.com/a/3661892/ with some extra fixes.
    /// </summary>
    /// <param name="sourceImage">The image to clone.</param>
    /// <returns>The cloned image.</returns>
    public static Bitmap CloneImage(Bitmap sourceImage)
    {
        Rectangle rect = new Rectangle(0, 0, sourceImage.Width, sourceImage.Height);
        Bitmap targetImage = new Bitmap(rect.Width, rect.Height, sourceImage.PixelFormat);
        targetImage.SetResolution(sourceImage.HorizontalResolution, sourceImage.VerticalResolution);
        BitmapData sourceData = sourceImage.LockBits(rect, ImageLockMode.ReadOnly, sourceImage.PixelFormat);
        BitmapData targetData = targetImage.LockBits(rect, ImageLockMode.WriteOnly, targetImage.PixelFormat);
        Int32 actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * rect.Width) + 7) / 8;
        Int32 h = sourceImage.Height;
        Int32 origStride = sourceData.Stride;
        Int32 targetStride = targetData.Stride;
        Byte[] imageData = new Byte[actualDataWidth];
        IntPtr sourcePos = sourceData.Scan0;
        IntPtr destPos = targetData.Scan0;
        // Copy line by line, skipping by stride but copying actual data width
        for (Int32 y = 0; y < h; y++)
        {
            Marshal.Copy(sourcePos, imageData, 0, actualDataWidth);
            Marshal.Copy(imageData, 0, destPos, actualDataWidth);
            sourcePos = new IntPtr(sourcePos.ToInt64() + origStride);
            destPos = new IntPtr(destPos.ToInt64() + targetStride);
        }
        targetImage.UnlockBits(targetData);
        sourceImage.UnlockBits(sourceData);
        // For indexed images, restore the palette. This is not linking to a referenced
        // object in the original image; the getter of Palette creates a new object when called.
        if ((sourceImage.PixelFormat & PixelFormat.Indexed) != 0)
            targetImage.Palette = sourceImage.Palette;
        // Restore DPI settings
        targetImage.SetResolution(sourceImage.HorizontalResolution, sourceImage.VerticalResolution);
        return targetImage;
    }

}

不过,这种方法似乎只能解决8位和4位png的问题.由Gimp重新保存的只有4种颜色的png变成了2位png,尽管没有任何透明度,但仍以32位颜色打开.

It seems this method only fixes the problem for 8-bit and 4-bit png, though. A png with only 4 colours re-saved by Gimp turned into a 2-bit png, and that still opened as 32-bit colour despite not containing any transparency.

保存调色板大小实际上存在一个类似的问题; .Net框架可以完美地处理非全尺寸调色板(8位小于256、4位小于16)加载png文件,但是在保存文件时会将其填充到完整调色板.可以通过类似的方法来解决此问题,方法是在保存到MemoryStream 之后对这些块进行后处理.不过,这将需要计算CRC.

There is in fact a similar issue with saving the palette size; the .Net framework can perfectly handle loading png files with a palette that's not the full size (less than 256 for 8-bit, less than 16 for 4-bit), but when saving the file it will pad it to the full palette. This can be fixed in a similar way, by post-processing the chunks after saving to a MemoryStream. This will require calculating the CRCs, though.

还要注意,尽管这应该能够加载任何图像类型,但是由于最后使用的CloneImage函数仅复制单个图像,因此它在动画GIF文件上将无法正常工作.

Also note that while this should be able to load any image type, it won't work correctly on animated GIF files, since the CloneImage function used at the end only copies a single image.

这篇关于C#-正确加载索引的彩色图像文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆