C# - Loading an indexed color image file correctly(C# - 正确加载索引彩色图像文件)
问题描述
所以我创建了一个索引颜色,每像素 8 位 PNG(如果格式正确,我已经用 ImageMagick 检查过)并且我想将它从磁盘加载到 System.Drawing.Bitmap
同时保持 8bpp 像素格式,以便查看(和操作)其调色板.但是,如果我像这样创建位图:
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");
生成的 Bitmap 会自动转换为 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 函数可以完美地编写这样的文件.相比之下,如果文件是 gif 格式,或者调色板 png 没有 透明度,则没有问题.
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 个字节(大端),然后是块 ID 的 4 个 ASCII 字符,然后是块内容本身,最后是一个 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 aMemoryStream
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 框架可以完美地处理加载带有非全尺寸调色板的 png 文件(8 位小于 256,4 位小于 16),但在保存文件时,它会将其填充到完整调色板.这可以通过类似的方式修复,通过在保存到 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.
另请注意,虽然这应该能够加载任何图像类型,但它无法在动画 GIF 文件上正常工作,因为最后使用的 CloneImage
函数仅复制单个图像.
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# - 正确加载索引彩色图像文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!