我也遇到了这个问题,似乎任何调色板的PNG图像包含透明度无法加载为 .Net 框架调色板,尽管 .Net 函数可以完美地write这样的文件。相反,如果文件是 gif 格式,或者调色板 png 具有no透明度。
调色板 png 中的透明度通过在标题中添加可选的“tRNS”块来实现,以指定每个调色板条目的 alpha。 .Net 类正确地读取和应用它,所以我真的不明白为什么他们坚持随后将图像转换为 32 位。
png 格式的结构相当简单;在标识字节之后,每个块是内容大小的 4 个字节(大端),然后是块 id 的 4 个 ASCII 字符,然后是块内容本身,最后是 4 字节块 CRC 值(同样,保存为 big-endian) - 字节序)。
鉴于这种结构,解决方案相当简单:
- 将文件读入字节数组。
- 通过分析标题确保它是调色板 png 文件。
- 通过从块头跳转到块头来找到“tRNS”块。
- 从块中读取 alpha 值。
- 创建一个包含图像数据的新字节数组,但删除“tRNS”块。
- 创建
Bitmap
对象使用MemoryStream
根据调整后的字节数据创建,从而产生正确的 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),但在保存文件时,它会将其填充到完整调色板。这可以通过类似的方式修复,通过在保存到后对块进行后处理MemoryStream https://stackoverflow.com/a/48165612/395685。不过,这需要计算 CRC。
另请注意,虽然这应该能够加载任何图像类型,但它无法在动画 GIF 文件上正常工作,因为CloneImage
最后使用的函数仅复制单个图像。