Created
September 28, 2023 06:56
-
-
Save QyQj/6c49f0bd521b25ba1e82c21f5203fe26 to your computer and use it in GitHub Desktop.
GrayPngBitmapLoader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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