問題: 當直接使用文件路徑載入8位灰度PNG圖片為Bitmap時,Bitmap的格式將會是Format32bppArgb,而不是Format8bppIndexed,這對一些判斷會有影響,所以需要手動解析PNG的數據來構造Bitmap 步驟 1. 判斷文件格式 若對PNG文件格式不是很瞭解,閱讀本文前 ...
問題:
當直接使用文件路徑載入8位灰度PNG圖片為Bitmap時,Bitmap的格式將會是Format32bppArgb,而不是Format8bppIndexed,這對一些判斷會有影響,所以需要手動解析PNG的數據來構造Bitmap
步驟
1. 判斷文件格式
若對PNG文件格式不是很瞭解,閱讀本文前可以參考PNG的文件格式 PNG文件格式詳解
簡而言之,PNG文件頭有8個固定位元組來標識它,他們是
private static byte[] PNG_IDENTIFIER = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
2. 判斷是否為8位灰度圖
識別為PNG文件後,需要判斷該PNG文件是否為8位的灰度圖
在PNG的文件頭標識後是PNG文件的第一個數據塊IHDR
,它的數據域由13個位元組組成
域的名稱 | 數據位元組數 | 說明 |
---|---|---|
Width | 4 bytes | 圖像寬度,以像素為單位 |
Height | 4 bytes | 圖像高度,以像素為單位 |
Bit depth | 1 byte | 圖像深度:索引彩色圖像:1,2,4或8 ;灰度圖像:1,2,4,8或16 ;真彩色圖像:8或16 |
ColorType | 1 byte | 顏色類型:0:灰度圖像, 1,2,4,8或16;2:真彩色圖像,8或16;3:索引彩色圖像,1,2,4或84:帶α通道數據的灰度圖像,8或16;6:帶α通道數據的真彩色圖像,8或16 |
Compression method | 1 byte | 壓縮方法(LZ77派生演算法) |
Filter method | 1 byte | 濾波器方法 |
Interlace method | 1 byte | 隔行掃描方法:0:非隔行掃描;1: Adam7(由Adam M. Costello開發的7遍隔行掃描方法) |
這裡我們看顏色深度以及顏色類型就行
var ihdrData = data[(PNG_IDENTIFIER.Length + 8)..(PNG_IDENTIFIER.Length + 8 + 13)];
var bitDepth = Convert.ToInt32(ihdrData[8]);
var colorType = Convert.ToInt32(ihdrData[9]);
這裡的data
是表示PNG文件的byte數組,+8是因為PNG文件的每個數據塊的數據域前都有4個位元組的數據域長度和4個位元組的數據塊類型(名稱)
3. 獲取全部圖像數據塊
PNG文件的圖像數據由一個或多個圖像數據塊IDAT
構成,並且他們是順序排列的
這裡通過while
迴圈找到所有的IDAT
塊
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;
}
4. 解壓DAT數據
上一步獲得的DAT數據是由Deflate
演算法壓縮後的,我們需要將它解壓縮,這裡使用.NET自帶的DeflateStream
進行解壓縮
IDAT的數據流以zlib格式存儲,結構為
名稱 | 長度 |
---|---|
zlib compression method/flags code | 1 byte |
Additional flags/check bits | 1 byte |
Compressed data blocks | n bytes |
Check value | 4 bytes |
解壓縮時去掉前2個位元組
var deCompressedDat = MicrosoftDecompress(compressedDat.ToArray()[2..]).AsSpan();
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;
}
5. 重建原始數據
PNG的IDAT
數據流在壓縮前會通過過濾演算法將原始數據進行過濾來提高壓縮率,這裡需要將過濾後的數據進行重建
有關過濾和重建可以參考W3組織的文檔
這裡定義了一個類來輔助重建
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
};
}
下麵獲取重建的數據
首先從IHDR
獲取寬高
var width = ihdrData.ReadBinaryInt(0, 4);
var height = ihdrData.ReadBinaryInt(4, 4);
按行處理
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);
}
6. 最後構建灰度Bitmap並賦予數據
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 bitmap;
完整代碼
參考:
1. PNG文件格式詳解
2. Png的數據解析
3. How to read 8-bit PNG image as 8-bit PNG image only?
4. Portable Network Graphics (PNG) Specification (Second Edition)