如何将8位PNG图像仅作为8位PNG图像读取?

3
我有一张8位PNG图片(见附件)。但是当我使用 Image.FromFile 方法读取它时,像素格式变成了32位。因此,我无法修改调色板。
请帮助我。
以下是我用于读取文件并更新调色板的代码:
    public static Image GetPreviewImage()
    {
        Bitmap updatedImage = null;
        try
        {
            // Reads the colors as a byte array
            byte[] paletteBytes = FetchColorPallette();

            updatedImage = Image.FromFile(@"C:\Screen-SaverBouncing.png");

            ColorPalette colorPalette = updatedImage.Palette;

            int j = 0;
            if (colorPalette.Entries.Length > 0)
            {
                for (int i = 0; i < paletteBytes.Length / 4; i++)
                {
                    Byte AValue = Convert.ToByte(paletteBytes[j]);
                    Byte RValue = Convert.ToByte(paletteBytes[j + 1]);
                    Byte GValue = Convert.ToByte(paletteBytes[j + 2]);
                    Byte BValue = Convert.ToByte(paletteBytes[j + 3]);
                    j += 4;

                    colorPalette.Entries[i] = Color.FromArgb(AValue, RValue, GValue, BValue);
                }
                updatedImage.Palette = colorPalette; ;
            }

            return updatedImage;
        }
        catch
        {
            throw;
        }
    }

你的代码没有展示如何读取图片。 - Thorsten Dittmar
问题在于“Screen-SaverBouncing.png”是一张8位图像(从Windows 7的属性窗口中可以看到),但colorPalette.Entries.Length始终为零。此外,如果我在IrfanViewer中打开此图像,我可以看到颜色调色板,修改它并保存它。如果我在这个新保存的文件上运行上面的代码,那么colorPalette.Entries.Length就是256。 - Vinay Venkataraman
现在问题已经解决了,因为问题出在 PNG 图像上而不是代码上。 - Vinay Venkataraman
嗯,我在使用C#保存每个PNG文件时都遇到了问题...你有什么想法吗? - Nyerguds
2个回答

3
我也遇到过这个问题,似乎任何包含透明度的调色板png图片都无法被.Net框架作为调色板文件加载,尽管.Net函数可以完美地写入这样的文件。相比之下,如果该文件是gif格式,则没有问题。
Png中的透明度通过在头部添加可选的“tRNS”块来实现,以指定每个调色板条目的alpha。 .Net类正确读取和应用此内容,因此我真的不理解为什么它们坚持在之后将图像转换为32位。更重要的是,即使透明块标记所有颜色为完全不透明,该bug也始终会发生。
Png格式的结构非常简单;在识别字节之后,每个块都是4个字节的内容大小,然后是4个ASCII字符的块id,然后是块内容本身,最后是4字节的块CRC值。
鉴于这种结构,解决方案相当简单:
  • 将文件读入字节数组。
  • 通过分析标头确保它是调色板png文件。
  • 通过从块标题跳到块标题找到“tRNS”块。
  • 从块中读取阿尔法值。
  • 创建一个新的字节数组,其中包含图像数据,但剪切了“tRNS”块。
  • 使用调整后的字节数据创建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 BitmapLoader
{
    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 can be found at http://www.libpng.org/pub/png/book/chapter08.html
    /// </summary>
    /// <param name="filename">Filename to load</param>
    /// <returns>The loaded image</returns>
    public static Bitmap LoadBitmap(String filename)
    {
        Byte[] data = File.ReadAllBytes(filename);
        return LoadBitmap(data);
    }

    /// <summary>
    /// Loads an image, checks if it is a PNG containing palette transparency, and if so, ensures it loads correctly.
    /// The theory 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;
                    }
                }
            }
        }
        Bitmap loadedImage;
        using (MemoryStream ms = new MemoryStream(data))
        using (Bitmap tmp = new Bitmap(ms))
            loadedImage = ImageUtils.CloneImage(tmp);
        ColorPalette pal = loadedImage.Palette;
        if (pal.Entries.Length == 0 || transparencyData == null)
            return loadedImage;
        for (Int32 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;
        return loadedImage;
    }

    /// <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 (chunkName.Length != 4 )
            throw new ArgumentException("Chunk must be 4 characters!", "chunkName");
        Byte[] chunkNamebytes = Encoding.ASCII.GetBytes(chunkName);
        if (chunkNamebytes.Length != 4)
            throw new ArgumentException("Chunk must be 4 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);
            // Alternative for more visual debugging:
            //String currentChunk = Encoding.ASCII.GetString(testBytes);
            //if (chunkName.Equals(currentChunk))
            //    return offset;
            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;
    }
}

提到的ImageUtils.CloneImage是目前我所知道的唯一一种百分百安全的方法来加载位图并断开与任何后备资源(如文件或流)的链接。可以在此处找到它。
另外,您也可以从MemoryStream创建图像并保持MemoryStream处于打开状态。显然,对于引用简单数组而不是外部资源的流,尽管这个IDisposable流处于打开状态,但其对于垃圾回收没有问题。这样做虽然不太整洁,但却更加简单。因此,创建loadedImage的代码就变成了:
MemoryStream ms = new MemoryStream(data)
Bitmap loadedImage = new Bitmap(ms);

1

我曾经遇到过类似的问题,需要读取保存在8bppIndexed像素格式中的PNG文件的8位值。

当您尝试使用new Bitmap(filename)读取8位索引PNG图像时,打开的文件是32位格式。

我的解决方案是使用lockbits并提供以下像素格式:

Bitmap b = new Bitmap(filename);
BitmapData data = b.LockBits(new Rectangle(0, 0, b.Width, b.Height),
                            ImageLockMode.ReadOnly, 
                            PixelFormat.Format8bppIndexed);

这样我就可以扫描数据并获得我保存的原始8位值。
希望这有所帮助。

哦,哇。那真的有效!但是,您仍然会失去颜色调色板以及其透明度信息,并且仍然需要使用原始的png数据来检查图像的原始颜色深度是否确实为8位。 - Nyerguds

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接