几个对于文件格式封装有帮助的函数

分享.NET by 达达 at 2006-04-14

年初研究了下GIF文件格式,最近又研究了下Flash文件格式,在对文件格式进行封装的时候经常遇到很多"不规则"按bit读取的情况,而.NET框架里的流最小单位只能读到byte读取,反反复复的写些Byte转换代码实在是烦人.

而且在研究Flash格式时发现,Flash里有些结构体的信息是不定长的,它有一个由几个bit表示的长度信息,用于表示后续的每个信息的bit长度,这种不定长的结构体减小的SWF文件的大小,但是也让我在封装SWF文件格式类库时有些郁闷.

先拿个例子看看,以下是Flash中矩形区域结构体的定义:

RECT
Field    Type    Comment
Nbits    UB[5]    Bits in each rect value field
Xmin    SB[Nbits]    x minimum position for rect
Xmax    SB[Nbits]    x maximum position for rect
Ymin    SB[Nbits]    y minimum position for rect
Ymax    SB[Nbits]    y maximum position for rect

注:UB表示无符号的不定长bit值,SB表示不定长的有符号bit值

由上述定义可以看出Nbits定义了后续的值占用的bit数,而Nbits自身占用5个bit.

举个Flash文件格式文档里的例子,假设一个RECT结构体的信息如下表示: 78 00 05 5F 00 00 0F A0 00

翻译成二进制是:

0111 1000 0000 0000 0000 0101 0101 1111 0000 0000
0000 0000 0000 1111 1010 0000 0000 0000

前5个bit取出来就是:01111 -> 15

说明后续每个值占用15个bit,每个值取出来如下:

000000000000000 -> 0 = Xmin
010101011111000 -> 11000 = Xmax
000000000000000 -> 0 = Ymin
001111101000000 -> 8000 = Ymax

这个过程我在代码里怎么处理的呢?

原先(以前)的做法如下:

写了一个BytesToBin函数,把读取的Byte数组转为二进制值,这个二进制值其实是以01组成的字符串. 代码如下:

        public static string BytesToBin(params byte[] bytes)
        {
            StringBuilder _result = new StringBuilder();

            foreach (byte data in bytes)
            {
                byte _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0,_doc % 2);
                        _doc = (byte)(_doc / 2);
                    }

                    _result.Append(_newStr.ToString().PadLeft(8, '0'));
                }
                else
                {
                    _result.Append("00000000");
                }
            }

            return _result.ToString();
        }

然后我使用Convert.ToInt32函数将字符串中间的几位截取并转为int值. 具体代码如下:

        public void Load(BinaryReader binReader)
        {
            Nbits = (byte)(binReader.PeekChar() >> 3);

            int _bitCount = Nbits * 4 + 5;
            int _byteCount = _bitCount / 8;

            if (_bitCount % 8 != 0) _byteCount++;

            byte[] _rectInfo = binReader.ReadBytes(_byteCount);

            string _bitStr = Common.Math.BytesToBin(_rectInfo);

            Xmin = Convert.ToInt32(_bitStr.Substring(5, Nbits), 2);
            Xmax = Convert.ToInt32(_bitStr.Substring(Nbits + 5, Nbits), 2);
            Ymin = Convert.ToInt32(_bitStr.Substring(Nbits * 2 + 5, Nbits), 2);
            Ymax = Convert.ToInt32(_bitStr.Substring(Nbits * 3 + 5, Nbits), 2);
        }

这样做看起来还可以,但是好像效率不高.

于是我写了以下两个函数,用于在从byte数组中截取所需要的中间几位的值

        public static UInt64 GetValueFromBits(byte[] bits, int start, int end)
        {
            if (bits == null || bits.Length == 0 
                || start < 0 || end > bits.Length - 1 || start > end)
                return 0;

            UInt64 _value = 0;
            int _length = 0;
            int _valueLength = end - start + 1;

            int _byteIndex = 0;
            byte _byteValue = 0;
            UInt64 _tempValue = 0;

            for (int i = 0; i < bits.Length; i++)
            {
                _byteIndex = i * 8;

                if (_byteIndex > end) break;
                if (_byteIndex + 7 < start) continue;

                _byteValue = bits[i];

                int _byteStart = start - _byteIndex;
                int _byteEnd = end - _byteIndex;

                if (_byteStart < 0) _byteStart = 0;

                if (_byteEnd > 7) _byteEnd = 7;
                else if (_byteEnd < 0) _byteEnd = _byteIndex - end;

                _byteValue = (byte)(_byteValue << _byteStart);
                _byteValue = (byte)(_byteValue >> _byteStart);
                _byteValue = (byte)(_byteValue >> (7 - _byteEnd));

                _length += _byteEnd - _byteStart + 1;

                _tempValue = _byteValue;

                _tempValue = _tempValue << (_valueLength - _length);

                _value = _value | _tempValue;
            }

            return _value;
        }

        public static UInt64[] GetValuesFromBits(byte[] bits, int start, int subLength, int valueCount)
        {
            if (bits == null || bits.Length == 0 
                || start < 0 || valueCount <= 0 || subLength <= 0 
                || start + subLength * valueCount > bits.Length)
                return null;

            UInt64[] _values = new UInt64[valueCount];

            for (int i = 0; i < valueCount; i++)
            {
                _values[i] = GetValueFromBits(bits, start + i * subLength, start + subLength + i * subLength - 1);
            }

            return _values;
        }

GetValueFromBits是从所给的byte数组bits中截取从start位到end位的值,这个位的顺序是从左到右以0为起始索引逐一递增的. 截取的数值包括start位和end位,这点需要留意.

GetValuesFromBits是从所给的byte数组bits中从start位开始以subLength为每个值的长度截取valueCount个的值.位的顺序也是从左到右以0为起始索引逐一递增的.

新的Load方法就可以改成:

        public void Load(BinaryReader binReader)
        {
            Nbits = (byte)(binReader.PeekChar() >> 3);

            int _bitCount = Nbits * 4 + 5;
            int _byteCount = _bitCount / 8;

            if (_bitCount % 8 != 0) _byteCount++;

            byte[] _rectInfo = binReader.ReadBytes(_byteCount);

            string _bitStr = Common.Math.BytesToBin(_rectInfo);

            Xmin = (int)BinMatch.GetValueFromBits(_rectInfo, 5, Nbits);
            Xmax = (int)BinMatch.GetValueFromBits(_rectInfo,Nbits + 5, Nbits), 2);
            Ymin = (int)BinMatch.GetValueFromBits(_rectInfo,Nbits * 2 + 5, Nbits), 2);
            Ymax = (int)BinMatch.GetValueFromBits(_rectInfo,Nbits * 3 + 5, Nbits), 2);
        }

以下给出我所写的用于对二进制数操作的一个类代码,里面其实就是一组我常用到的二进制取值之类的函数. 注:不好意思最近没什么时间,所以就没有写注释了,大家凑合着看吧,有问题或者建议可以留言告诉我

    public class BinMath
    {
        public static string BytesToBin(params byte[] bytes)
        {
            StringBuilder _result = new StringBuilder();

            foreach (byte data in bytes)
            {
                byte _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0,_doc % 2);
                        _doc = (byte)(_doc / 2);
                    }

                    _result.Append(_newStr.ToString().PadLeft(8, '0'));
                }
                else
                {
                    _result.Append("00000000");
                }
            }

            return _result.ToString();
        }

        public static string UInt16sToBin(params UInt16[] integers)
        {
            StringBuilder _result = new StringBuilder();

            foreach (UInt16 data in integers)
            {
                UInt16 _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0, _doc % 2);
                        _doc = (UInt16)(_doc / 2);
                    }

                    _result.Append(_newStr.ToString().PadLeft(16, '0'));
                }
                else
                {
                    _result.Append("0000000000000000");
                }
            }

            return _result.ToString();
        }
        
        public static string Int16sToBin(params Int16[] integers)
        {
            StringBuilder _result = new StringBuilder();

            foreach (Int16 data in integers)
            {
                Int16 _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0, _doc % 2);
                        _doc = (Int16)(_doc / 2);
                    }

                    _result.Append(_newStr.ToString().PadLeft(16, '0'));
                }
                else
                {
                    _result.Append("0000000000000000");
                }
            }

            return _result.ToString();
        }

        public static string UInt32sToBin(params UInt32[] integers)
        {
            StringBuilder _result = new StringBuilder();

            foreach (UInt32 data in integers)
            {
                UInt32 _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0, _doc % 2);
                        _doc = _doc / 2;
                    }

                    _result.Append(_newStr.ToString().PadLeft(32, '0'));
                }
                else
                {
                    _result.Append("00000000000000000000000000000000");
                }
            }

            return _result.ToString();
        }

        public static string Int32sToBin(params Int32[] integers)
        {
            StringBuilder _result = new StringBuilder();

            foreach (Int32 data in integers)
            {
                Int32 _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0, _doc % 2);
                        _doc = _doc / 2;
                    }

                    _result.Append(_newStr.ToString().PadLeft(32, '0'));
                }
                else
                {
                    _result.Append("00000000000000000000000000000000");
                }
            }

            return _result.ToString();
        }

        public static string UInt64sToBin(params UInt64[] integers)
        {
            StringBuilder _result = new StringBuilder();

            foreach (UInt64 data in integers)
            {
                UInt64 _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0, _doc % 2);
                        _doc = _doc / 2;
                    }

                    _result.Append(_newStr.ToString().PadLeft(64, '0'));
                }
                else
                {
                    _result.Append("0000000000000000000000000000000000000000000000000000000000000000");
                }
            }

            return _result.ToString();
        }

        public static string Int64sToBin(params Int64[] integers)
        {
            StringBuilder _result = new StringBuilder();

            foreach (Int64 data in integers)
            {
                Int64 _doc = data;

                if (_doc != 0)
                {
                    StringBuilder _newStr = new StringBuilder();

                    while (_doc != 0)
                    {
                        _newStr.Insert(0, _doc % 2);
                        _doc = _doc / 2;
                    }

                    _result.Append(_newStr.ToString().PadLeft(64, '0'));
                }
                else
                {
                    _result.Append("0000000000000000000000000000000000000000000000000000000000000000");
                }
            }

            return _result.ToString();
        }

        public static Byte GetByteBits(Byte source, byte start, byte end)
        {
            if (start >= 0 && start <= 7 && end >= 0 && end <= 7)
            {
                Byte _toolNum = 0xFF;

                _toolNum = (Byte)(_toolNum >> start);
                _toolNum = (Byte)(_toolNum << start);
                _toolNum = (Byte)(_toolNum << (7 - end));
                _toolNum = (Byte)(_toolNum >> (7 - end));

                return (Byte)((source & _toolNum) >> start);
            }

            return 0;
        }

        public static UInt16 GetUInt16Bits(UInt16 source, byte start, byte end)
        {
            if (start >= 0 && start <= 15 && end >= 0 && end <= 15)
            {
                UInt16 _toolNum = 0xFFFF;

                _toolNum = (UInt16)(_toolNum >> start);
                _toolNum = (UInt16)(_toolNum << start);
                _toolNum = (UInt16)(_toolNum << (15 - end));
                _toolNum = (UInt16)(_toolNum >> (15 - end));

                return (UInt16)((source & _toolNum) >> start);
            }

            return 0;
        }

        public static UInt32 GetUInt32Bits(UInt32 source, byte start, byte end)
        {
            if (start >= 0 && start <= 31 && end >= 0 && end <= 31)
            {
                UInt32 _toolNum = 0xFFFFFFFF;

                _toolNum = _toolNum >> start;
                _toolNum = _toolNum << start;
                _toolNum = _toolNum << (31 - end);
                _toolNum = _toolNum >> (31 - end);

                return (source & _toolNum) >> start;
            }

            return 0;
        }

        public static UInt64 GetUInt64Bits(UInt64 source, byte start, byte end)
        {
            if (start >= 0 && start <= 63 && end >= 0 && end <= 63)
            {
                UInt64 _toolNum = 0xFFFFFFFFFFFFFFFF;

                _toolNum = _toolNum >> start;
                _toolNum = _toolNum << start;
                _toolNum = _toolNum << (63 - end);
                _toolNum = _toolNum >> (63 - end);

                return (source & _toolNum) >> start;
            }

            return 0;
        }

        public static UInt64 GetValueFromBits(byte[] bits, int start, int end)
        {
            if (bits == null || bits.Length == 0 
                || start < 0 || end > bits.Length - 1 || start > end)
                return 0;

            UInt64 _value = 0;
            int _length = 0;
            int _valueLength = end - start + 1;

            int _byteIndex = 0;
            byte _byteValue = 0;
            UInt64 _tempValue = 0;

            for (int i = 0; i < bits.Length; i++)
            {
                _byteIndex = i * 8;

                if (_byteIndex > end) break;
                if (_byteIndex + 7 < start) continue;

                _byteValue = bits[i];

                int _byteStart = start - _byteIndex;
                int _byteEnd = end - _byteIndex;

                if (_byteStart < 0) _byteStart = 0;

                if (_byteEnd > 7) _byteEnd = 7;
                else if (_byteEnd < 0) _byteEnd = _byteIndex - end;

                _byteValue = (byte)(_byteValue << _byteStart);
                _byteValue = (byte)(_byteValue >> _byteStart);
                _byteValue = (byte)(_byteValue >> (7 - _byteEnd));

                _length += _byteEnd - _byteStart + 1;

                _tempValue = _byteValue;

                _tempValue = _tempValue << (_valueLength - _length);

                _value = _value | _tempValue;
            }

            return _value;
        }

        public static UInt64[] GetValuesFromBits(byte[] bits, int start, int subLength, int valueCount)
        {
            if (bits == null || bits.Length == 0 
                || start < 0 || valueCount <= 0 || subLength <= 0 
                || start + subLength * valueCount > bits.Length)
                return null;

            UInt64[] _values = new UInt64[valueCount];

            for (int i = 0; i < valueCount; i++)
            {
                _values[i] = GetValueFromBits(bits, start + i * subLength, start + subLength + i * subLength - 1);
            }

            return _values;
        }
    }