Skip to content

Instantly share code, notes, and snippets.

@QyQj
Created September 28, 2023 06:56
Show Gist options
  • Save QyQj/6c49f0bd521b25ba1e82c21f5203fe26 to your computer and use it in GitHub Desktop.
Save QyQj/6c49f0bd521b25ba1e82c21f5203fe26 to your computer and use it in GitHub Desktop.
GrayPngBitmapLoader
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))
{
//检查是否是8为灰度图
var ihdrData = data[(PNG_IDENTIFIER.Length + 8)..(PNG_IDENTIFIER.Length + 8 + 13)];
var bitDepth = Convert.ToInt32(ihdrData[8]);
var colorType = Convert.ToInt32(ihdrData[9]);
if (bitDepth == 8 && colorType == 0)
{
var compressedSubDats = new List<byte[]>();
var firstDatOffset = FindChunk(data, "IDAT");
var firstDatLength = GetChunkDataLength(data, firstDatOffset);
var firstDat = new byte[firstDatLength];
Array.Copy(data, firstDatOffset + 8, firstDat, 0, firstDatLength);
compressedSubDats.Add(firstDat);
var dataSpan = data.AsSpan().Slice(firstDatOffset + 12 + firstDatLength);
while (Encoding.ASCII.GetString(dataSpan[4..8]) == "IDAT")
{
var datLength = dataSpan.ReadBinaryInt(0, 4);
var dat = new byte[datLength];
dataSpan.Slice(8, datLength).CopyTo(dat);
compressedSubDats.Add(dat);
dataSpan = dataSpan.Slice(12 + datLength);
}
var compressedDatLength = compressedSubDats.Sum(a => a.Length);
var compressedDat = new byte[compressedDatLength].AsSpan();
var index = 0;
for (int i = 0; i < compressedSubDats.Count; i++)
{
var subDat = compressedSubDats[i];
subDat.CopyTo(compressedDat.Slice(index, subDat.Length));
index += subDat.Length;
}
var width = ihdrData.ReadBinaryInt(0, 4);
var height = ihdrData.ReadBinaryInt(4, 4);
var deCompressedDat = MicrosoftDecompress(compressedDat.ToArray()[2..]).AsSpan();
var filtRowDic = new Dictionary<int, byte[]>();
for (int i = 0; i < height; i++)
{
var rowData = deCompressedDat.Slice(i * (width + 1), (width + 1));
filtRowDic.Add(i, rowData.ToArray());
}
var rowColDic = new Dictionary<(int, int), PngFilterByte>();
for (int i = 0; i < height; i++)
{
var row = filtRowDic[i];
var filterType = row[0];
for (int j = 1; j <= width; j++)
{
var bt = new PngFilterByte(filterType, i, j - 1)
{
Filt = Convert.ToInt32(row[j]),
IsFiltered = true,
IsTop = i == 0,
IsLeft = j == 1
};
if (bt.IsTop && bt.IsLeft)
{
bt.C = PngFilterByte.Zero;
}
if (!bt.IsTop)
{
bt.B = rowColDic[(bt.Row - 1, bt.Column)];
}
if (!bt.IsLeft)
{
bt.A = rowColDic[(bt.Row, bt.Column - 1)];
}
rowColDic.Add((bt.Row, bt.Column), bt);
}
}
var realImageData = new byte[rowColDic.Count];
foreach (var bt in rowColDic.Values)
{
realImageData[bt.Row * width + bt.Column] = Convert.ToByte(bt.Recon);
}
using var bitmap = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
ColorPalette cp = bitmap.Palette;
for (int i = 0; i < 256; i++)
{
cp.Entries[i] = Color.FromArgb(i, i, i);
}
bitmap.Palette = cp;
var bmpData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
Marshal.Copy(realImageData, 0, bmpData.Scan0, realImageData.Length);
bitmap.UnlockBits(bmpData);
return Utils.CloneImage(bitmap);
}
// Check if it contains a palette.
// I'm sure it can be looked up in the header somehow, but meh.
int plteOffset = FindChunk(data, "PLTE");
if (plteOffset != -1)
{
// Check if it contains a palette transparency chunk.
int trnsOffset = FindChunk(data, "tRNS");
if (trnsOffset != -1)
{
// Get chunk
int 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);
int 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 = Utils.CloneImage(tmp);
ColorPalette pal = loadedImage.Palette;
if (pal.Entries.Length == 0 || transparencyData == null)
return loadedImage;
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;
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 int 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");
int offset = PNG_IDENTIFIER.Length;
int 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;
int chunkLength = GetChunkDataLength(data, offset);
// chunk size + chunk header + chunk checksum = 12 bytes.
offset += 12 + chunkLength;
}
return -1;
}
private static int GetChunkDataLength(byte[] data, int 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.
int 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;
}
public static byte[] MicrosoftDecompress(byte[] data)
{
MemoryStream compressed = new MemoryStream(data);
MemoryStream decompressed = new MemoryStream();
DeflateStream deflateStream = new DeflateStream(compressed, CompressionMode.Decompress); // 注意: 这里第一个参数同样是填写压缩的数据,但是这次是作为输入的数据
deflateStream.CopyTo(decompressed);
byte[] result = decompressed.ToArray();
return result;
}
}
public class PngFilterByte
{
public PngFilterByte(int filterType, int row, int col)
{
FilterType = filterType;
Row = row;
Column = col;
}
public int Row { get; set; }
public int Column { get; set; }
public int FilterType { get; set; }
public PngFilterByte C { get; set; }
public PngFilterByte B { get; set; }
public PngFilterByte A { get; set; }
public int X { get; set; }
private bool _isTop;
public bool IsTop
{
get => _isTop;
init
{
_isTop = value;
if (!_isTop) return;
B = Zero;
}
}
private bool _isLeft;
public bool IsLeft
{
get => _isLeft;
init
{
_isLeft = value;
if (!_isLeft) return;
A = Zero;
}
}
public int _filt;
public int Filt
{
get => IsFiltered ? _filt : DoFilter();
init
{
_filt = value;
}
}
public bool IsFiltered { get; set; } = false;
public int DoFilter()
{
_filt = FilterType switch
{
0 => X,
1 => X - A.X,
2 => X - B.X,
3 => X - (int)Math.Floor((A.X + B.X) / 2.0M),
4 => X - Paeth(A.X, B.X, C.X),
_ => X
};
if (_filt > 255) _filt %= 256;
IsFiltered = true;
return _filt;
}
private int _recon;
public int Recon
{
get => IsReconstructed ? _recon : DoReconstruction();
init
{
_filt = value;
}
}
public bool IsReconstructed { get; set; } = false;
public int DoReconstruction()
{
_recon = FilterType switch
{
0 => Filt,
1 => Filt + A.Recon,
2 => Filt + B.Recon,
3 => Filt + (int)Math.Floor((A.Recon + B.Recon) / 2.0M),
4 => Filt + Paeth(A.Recon, B.Recon, C.Recon),
_ => Filt
};
if (_recon > 255) _recon %= 256;
X = _recon;
IsReconstructed = true;
return _recon;
}
private int Paeth(int a, int b, int c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
if (pa <= pb && pa <= pc)
{
return a;
}
else if (pb <= pc)
{
return b;
}
else
{
return c;
}
}
public static PngFilterByte Zero = new PngFilterByte(0, -1, -1)
{
IsFiltered = true,
IsReconstructed = true,
X = 0,
Filt = 0,
Recon = 0
};
}
public static class Utils
{
public static int ReadBinaryInt(this byte[] bytes, int startIndex, int length)
{
var finalBinaryString = ReadBinaryIntString(bytes, startIndex, length);
return Convert.ToInt32(finalBinaryString, 2);
}
public static int ReadBinaryInt(this Span<byte> bytes, int startIndex, int length)
{
var finalBinaryString = ReadBinaryIntString(bytes, startIndex, length);
return Convert.ToInt32(finalBinaryString, 2);
}
public static string ReadBinaryIntString(this byte[] bytes, int startIndex, int length)
{
var finalBinaryString = string.Empty;
for (int i = 0; i < length; i++)
{
var binaryString = Convert.ToString(bytes[startIndex + i], 2);
binaryString = binaryString.PadLeft(8, '0');
finalBinaryString += binaryString;
}
return finalBinaryString;
}
public static string ReadBinaryIntString(this Span<byte> bytes, int startIndex, int length)
{
var finalBinaryString = string.Empty;
for (int i = 0; i < length; i++)
{
var binaryString = Convert.ToString(bytes[startIndex + i], 2);
binaryString = binaryString.PadLeft(8, '0');
finalBinaryString += binaryString;
}
return finalBinaryString;
}
/// <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);
int actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * rect.Width) + 7) / 8;
int h = sourceImage.Height;
int origStride = sourceData.Stride;
bool isFlipped = origStride < 0;
origStride = Math.Abs(origStride); // Fix for negative stride in BMP format.
int 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 (int 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);
// Fix for negative stride on BMP format.
if (isFlipped)
targetImage.RotateFlip(RotateFlipType.Rotate180FlipX);
// 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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment