C# 手动解析灰度PNG图片为Bitmap

  • C# 手动解析灰度PNG图片为Bitmap已关闭评论
  • 128 次浏览
  • A+
所属分类:.NET技术
摘要

当直接使用文件路径加载8位灰度PNG图片为Bitmap时,Bitmap的格式将会是Format32bppArgb,而不是Format8bppIndexed,这对一些判断会有影响,所以需要手动解析PNG的数据来构造Bitmap


问题:

当直接使用文件路径加载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; 

完整代码

Github Gist

参考:

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)