翼度科技»论坛 编程开发 .net 查看内容

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

9

主题

9

帖子

27

积分

新手上路

Rank: 1

积分
27
问题:

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

1. 判断文件格式

若对PNG文件格式不是很了解,阅读本文前可以参考PNG的文件格式 PNG文件格式详解
简而言之,PNG文件头有8个固定字节来标识它,他们是
  1. private static byte[] PNG_IDENTIFIER = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
复制代码
2. 判断是否为8位灰度图

识别为PNG文件后,需要判断该PNG文件是否为8位的灰度图
在PNG的文件头标识后是PNG文件的第一个数据块IHDR,它的数据域由13个字节组成
域的名称数据字节数说明Width4 bytes图像宽度,以像素为单位Height4 bytes图像高度,以像素为单位Bit depth1 byte图像深度:索引彩色图像:1,2,4或8 ;灰度图像:1,2,4,8或16 ;真彩色图像:8或16ColorType1 byte颜色类型:0:灰度图像, 1,2,4,8或16;2:真彩色图像,8或16;3:索引彩色图像,1,2,4或84:带α通道数据的灰度图像,8或16;6:带α通道数据的真彩色图像,8或16Compression method1 byte压缩方法(LZ77派生算法)Filter method1 byte滤波器方法Interlace method1 byte隔行扫描方法:0:非隔行扫描;1: Adam7(由Adam M. Costello开发的7遍隔行扫描方法)这里我们看颜色深度以及颜色类型就行
  1. var ihdrData = data[(PNG_IDENTIFIER.Length + 8)..(PNG_IDENTIFIER.Length + 8 + 13)];
  2. var bitDepth = Convert.ToInt32(ihdrData[8]);
  3. var colorType = Convert.ToInt32(ihdrData[9]);
复制代码
这里的data是表示PNG文件的byte数组,+8是因为PNG文件的每个数据块的数据域前都有4个字节的数据域长度和4个字节的数据块类型(名称)

3. 获取全部图像数据块

PNG文件的图像数据由一个或多个图像数据块IDAT构成,并且他们是顺序排列的
这里通过while循环找到所有的IDAT块
  1. var compressedSubDats = new List<byte[]>();
  2. var firstDatOffset = FindChunk(data, "IDAT");
  3. var firstDatLength = GetChunkDataLength(data, firstDatOffset);
  4. var firstDat = new byte[firstDatLength];
  5. Array.Copy(data, firstDatOffset + 8, firstDat, 0, firstDatLength);
  6. compressedSubDats.Add(firstDat);
  7. var dataSpan = data.AsSpan().Slice(firstDatOffset + 12 + firstDatLength);
  8. while (Encoding.ASCII.GetString(dataSpan[4..8]) == "IDAT")
  9. {
  10.     var datLength = dataSpan.ReadBinaryInt(0, 4);
  11.     var dat = new byte[datLength];
  12.     dataSpan.Slice(8, datLength).CopyTo(dat);
  13.     compressedSubDats.Add(dat);
  14.     dataSpan = dataSpan.Slice(12 + datLength);
  15. }
  16. var compressedDatLength = compressedSubDats.Sum(a => a.Length);
  17. var compressedDat = new byte[compressedDatLength].AsSpan();
  18. var index = 0;
  19. for (int i = 0; i < compressedSubDats.Count; i++)
  20. {
  21.     var subDat = compressedSubDats[i];
  22.     subDat.CopyTo(compressedDat.Slice(index, subDat.Length));
  23.     index += subDat.Length;
  24. }
复制代码
4. 解压DAT数据

上一步获得的DAT数据是由Deflate算法压缩后的,我们需要将它解压缩,这里使用.NET自带的DeflateStream进行解压缩
IDAT的数据流以zlib格式存储,结构为
名称长度zlib compression method/flags code1 byteAdditional flags/check bits1 byteCompressed data blocksn bytesCheck value4 bytes解压缩时去掉前2个字节
  1. var deCompressedDat = MicrosoftDecompress(compressedDat.ToArray()[2..]).AsSpan();
复制代码
  1. public static byte[] MicrosoftDecompress(byte[] data)
  2. {
  3.     MemoryStream compressed = new MemoryStream(data);
  4.     MemoryStream decompressed = new MemoryStream();
  5.     DeflateStream deflateStream = new DeflateStream(compressed, CompressionMode.Decompress);
  6.     deflateStream.CopyTo(decompressed);
  7.     byte[] result = decompressed.ToArray();
  8.     return result;
  9. }
复制代码
5. 重建原始数据

PNG的IDAT数据流在压缩前会通过过滤算法将原始数据进行过滤来提高压缩率,这里需要将过滤后的数据进行重建
有关过滤和重建可以参考W3组织的文档
这里定义了一个类来辅助重建
[code]    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

举报 回复 使用道具