C# - 正确加载索引彩色图像文件

标签 c# image bitmap

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

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

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

“How to convert a 32bpp image to 8bpp in C#?”这个问题的答案StackOverflow 上说这可能是将其转换回 8bpp 的有效方法:

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

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

我如何才能将图像原生加载到 8bpp,或者至少正确地将 32bpp 的图像转换为 8bpp?

最佳答案

我也有这个问题,似乎任何包含透明度的调色板 png 图像 都无法加载为被 .Net 框架调色,尽管 .Net 函数可以完美地加载这样一个文件。相反,如果文件是 gif 格式,或者如果调色板 png 没有透明度,则没有问题。

调色板 png 中的透明度通过在标题中添加一个可选的“tRNS” block 来工作,以指定每个调色板条目的 alpha。 .Net 类可以正确读取和应用它,所以我真的不明白为什么他们坚持要在之后将图像转换为 32 位。

png格式的结构相当简单;在标识字节之后,每个 block 是内容大小的 4 个字节(big-endian),然后是 block ID 的 4 个 ASCII 字符,然后是 block 内容本身,最后是一个 4 字节的 block CRC 值(同样,保存为 big -endian)。

鉴于这种结构,解决方案相当简单:

  • 将文件读入字节数组。
  • 通过分析标题确保它是一个调色板 png 文件。
  • 通过从 block 头跳到 block 头找到“tRNS” block 。
  • 从 block 中读取 alpha 值。
  • 创建一个包含图像数据的新字节数组,但删除“tRNS” block 。
  • 使用根据调整后的字节数据创建的 MemoryStream 创建 Bitmap 对象,从而生成正确的 8 位图像。
  • 使用提取的 alpha 数据修复调色板。

如果您正确地进行了检查和回退,您可以使用此函数加载任何图像,如果它恰好被识别为带有透明度信息的调色板 png,它将执行修复。

/// <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 位颜色打开。

实际上保存调色板大小也有类似的问题; .Net 框架可以完美地处理使用非完整大小的调色板加载 png 文件(8 位小于 256,4 位小于 16),但是在保存文件时它会将其填充到完整调色板。这可以用类似的方式解决,by post-processing the chunks after saving to a MemoryStream .不过,这将需要计算 CRC。

另请注意,虽然这应该能够加载任何图像类型,但它无法在动画 GIF 文件上正常工作,因为最后使用的 CloneImage 函数仅复制单个图像。

关于C# - 正确加载索引彩色图像文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44835726/

相关文章:

c# - 隐式延迟加载与显式延迟加载

jquery - 我的图像保持垂直,而不是水平

java - 如何在Java中将TIFF转换为JPEG/PNG

java - AndEngine加载图形: Where Do I put my assets folder and my resources?

android - 将字符串转换为 uri 为位图以显示在 ImageView 中

java-me - 创建原生位图库

c# - 为下次运行应用程序保存文本框值

c# - Windows (xp) 中使用 C# 或 C 进行以太网帧或 MAC 广播

c# - 有没有一种方法可以简化检查值是某个值而不是 0 的倍数?

iphone - HTML5 iPhone动态缓存图片