//#define mgGIF_UNSAFE using UnityEngine; using System; using System.Runtime.CompilerServices; using System.Text; using System.Runtime.InteropServices; // unsafe namespace MG.GIF { //////////////////////////////////////////////////////////////////////////////// public class Image : ICloneable { public int Width; public int Height; public int Delay; // milliseconds public Color32[] RawImage; public Image() { } public Image( Image img ) { Width = img.Width; Height = img.Height; Delay = img.Delay; RawImage = img.RawImage != null ? (Color32[]) img.RawImage.Clone() : null; } public object Clone() { return new Image( this ); } public Texture2D CreateTexture() { var tex = new Texture2D( Width, Height, TextureFormat.ARGB32, false ) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp }; tex.SetPixels32( RawImage ); tex.Apply(); return tex; } } //////////////////////////////////////////////////////////////////////////////// #if mgGIF_UNSAFE unsafe #endif public class Decoder : IDisposable { public string Version; public ushort Width; public ushort Height; public Color32 BackgroundColour; //------------------------------------------------------------------------------ // GIF format enums [Flags] enum ImageFlag { Interlaced = 0x40, ColourTable = 0x80, TableSizeMask = 0x07, BitDepthMask = 0x70, } enum Block { Image = 0x2C, Extension = 0x21, End = 0x3B } enum Extension { GraphicControl = 0xF9, Comments = 0xFE, PlainText = 0x01, ApplicationData = 0xFF } enum Disposal { None = 0x00, DoNotDispose = 0x04, RestoreBackground = 0x08, ReturnToPrevious = 0x0C } [Flags] enum ControlFlags { HasTransparency = 0x01, DisposalMask = 0x0C } //------------------------------------------------------------------------------ const uint NoCode = 0xFFFF; const ushort NoTransparency = 0xFFFF; // input stream to decode byte[] Input; int D; // colour table Color32[] GlobalColourTable; Color32[] LocalColourTable; Color32[] ActiveColourTable; ushort TransparentIndex; // current image Image Image = new Image(); ushort ImageLeft; ushort ImageTop; ushort ImageWidth; ushort ImageHeight; Color32[] Output; Color32[] PreviousImage; readonly int[] Pow2 = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 }; //------------------------------------------------------------------------------ // ctor public Decoder( byte[] data ) : this() { Load( data ); } public Decoder Load( byte[] data ) { Input = data; D = 0; GlobalColourTable = new Color32[ 256 ]; LocalColourTable = new Color32[ 256 ]; TransparentIndex = NoTransparency; Output = null; PreviousImage = null; Image.Delay = 0; return this; } //------------------------------------------------------------------------------ // reading data utility functions [MethodImpl( MethodImplOptions.AggressiveInlining )] byte ReadByte() { return Input[ D++ ]; } [MethodImpl( MethodImplOptions.AggressiveInlining )] ushort ReadUInt16() { return (ushort) ( Input[ D++ ] | Input[ D++ ] << 8 ); } //------------------------------------------------------------------------------ void ReadHeader() { if( Input == null || Input.Length <= 12 ) { throw new Exception( "Invalid data" ); } // signature Version = Encoding.ASCII.GetString( Input, 0, 6 ); D = 6; if( Version != "GIF87a" && Version != "GIF89a" ) { throw new Exception( "Unsupported GIF version" ); } // read header Width = ReadUInt16(); Height = ReadUInt16(); Image.Width = Width; Image.Height = Height; var flags = (ImageFlag) ReadByte(); var bgIndex = ReadByte(); // background colour ReadByte(); // aspect ratio if( flags.HasFlag( ImageFlag.ColourTable ) ) { ReadColourTable( GlobalColourTable, flags ); } BackgroundColour = GlobalColourTable[ bgIndex ]; } //------------------------------------------------------------------------------ public Image NextImage() { // if at start of data, read header if( D == 0 ) { ReadHeader(); } // read blocks until we find an image block while( true ) { var block = (Block) ReadByte(); switch( block ) { case Block.Image: { // return the image if we got one var img = ReadImageBlock(); if( img != null ) { return img; } } break; case Block.Extension: { var ext = (Extension) ReadByte(); if( ext == Extension.GraphicControl ) { ReadControlBlock(); } else { SkipBlocks(); } } break; case Block.End: { // end block - stop! return null; } default: { throw new Exception( "Unexpected block type" ); } } } } //------------------------------------------------------------------------------ Color32[] ReadColourTable( Color32[] colourTable, ImageFlag flags ) { var tableSize = Pow2[ (int)( flags & ImageFlag.TableSizeMask ) + 1 ]; for( var i = 0; i < tableSize; i++ ) { colourTable[ i ] = new Color32( Input[ D++ ], Input[ D++ ], Input[ D++ ], 0xFF ); } return colourTable; } //------------------------------------------------------------------------------ void SkipBlocks() { var blockSize = Input[ D++ ]; while( blockSize != 0x00 ) { D += blockSize; blockSize = Input[ D++ ]; } } //------------------------------------------------------------------------------ void ReadControlBlock() { // read block ReadByte(); // block size (0x04) var flags = (ControlFlags) ReadByte(); // flags Image.Delay = ReadUInt16() * 10; // delay (1/100th -> milliseconds) var transparentColour = ReadByte(); // transparent colour ReadByte(); // terminator (0x00) // has transparent colour? if( flags.HasFlag( ControlFlags.HasTransparency ) ) { TransparentIndex = transparentColour; } else { TransparentIndex = NoTransparency; } // dispose of current image switch( (Disposal)( flags & ControlFlags.DisposalMask ) ) { default: case Disposal.None: case Disposal.DoNotDispose: // remember current image in case we need to "return to previous" PreviousImage = Output; break; case Disposal.RestoreBackground: // empty image - don't track Output = new Color32[ Width * Height ]; break; case Disposal.ReturnToPrevious: // return to previous image Output = new Color32[ Width * Height ]; if( PreviousImage != null ) { Array.Copy( PreviousImage, Output, Output.Length ); } break; } } //------------------------------------------------------------------------------ Image ReadImageBlock() { // read image block header ImageLeft = ReadUInt16(); ImageTop = ReadUInt16(); ImageWidth = ReadUInt16(); ImageHeight = ReadUInt16(); var flags = (ImageFlag) ReadByte(); // bad image if we don't have any dimensions if( ImageWidth == 0 || ImageHeight == 0 ) { return null; } // read colour table if( flags.HasFlag( ImageFlag.ColourTable ) ) { ActiveColourTable = ReadColourTable( LocalColourTable, flags ); } else { ActiveColourTable = GlobalColourTable; } if( Output == null ) { Output = new Color32[ Width * Height ]; PreviousImage = Output; } // read image data DecompressLZW(); // deinterlace if( flags.HasFlag( ImageFlag.Interlaced ) ) { Deinterlace(); } // return image Image.RawImage = Output; return Image; } //------------------------------------------------------------------------------ // decode interlaced images void Deinterlace() { var numRows = Output.Length / Width; var writePos = Output.Length - Width; // NB: work backwards due to Y-coord flip var input = Output; Output = new Color32[ Output.Length ]; for( var row = 0; row < numRows; row++ ) { int copyRow; // every 8th row starting at 0 if( row % 8 == 0 ) { copyRow = row / 8; } // every 8th row starting at 4 else if( ( row + 4 ) % 8 == 0 ) { var o = numRows / 8; copyRow = o + ( row - 4 ) / 8; } // every 4th row starting at 2 else if( ( row + 2 ) % 4 == 0 ) { var o = numRows / 4; copyRow = o + ( row - 2 ) / 4; } // every 2nd row starting at 1 else // if( ( r + 1 ) % 2 == 0 ) { var o = numRows / 2; copyRow = o + ( row - 1 ) / 2; } Array.Copy( input, ( numRows - copyRow - 1 ) * Width, Output, writePos, Width ); writePos -= Width; } } //------------------------------------------------------------------------------ // DecompressLZW() #if mgGIF_UNSAFE bool Disposed = false; int CodesLength; IntPtr CodesHandle; ushort* pCodes; IntPtr CurBlock; uint* pCurBlock; const int MaxCodes = 4096; IntPtr Indices; ushort** pIndicies; public Decoder() { // unmanaged allocations CodesLength = 128 * 1024; CodesHandle = Marshal.AllocHGlobal( CodesLength * sizeof( ushort ) ); pCodes = (ushort*) CodesHandle.ToPointer(); CurBlock = Marshal.AllocHGlobal( 64 * sizeof( uint ) ); pCurBlock = (uint*) CurBlock.ToPointer(); Indices = Marshal.AllocHGlobal( MaxCodes * sizeof( ushort* ) ); pIndicies = (ushort**) Indices.ToPointer(); } protected virtual void Dispose( bool disposing ) { if( Disposed ) { return; } // release unmanaged resources Marshal.FreeHGlobal( CodesHandle ); Marshal.FreeHGlobal( CurBlock ); Marshal.FreeHGlobal( Indices ); Disposed = true; } ~Decoder() { Dispose( false ); } public void Dispose() { Dispose( true ); GC.SuppressFinalize( this ); } void DecompressLZW() { var pCodeBufferEnd = pCodes + CodesLength; fixed( byte* pData = Input ) { fixed( Color32* pOutput = Output, pColourTable = ActiveColourTable ) { var row = ( Height - ImageTop - 1 ) * Width; // start at end of array as we are reversing the row order var safeWidth = ImageLeft + ImageWidth > Width ? Width - ImageLeft : ImageWidth; var pWrite = &pOutput[ row + ImageLeft ]; var pRow = pWrite; var pRowEnd = pWrite + ImageWidth; var pImageEnd = pWrite + safeWidth; // setup codes int minimumCodeSize = Input[ D++ ]; if( minimumCodeSize > 11 ) { minimumCodeSize = 11; } var codeSize = minimumCodeSize + 1; var nextSize = Pow2[ codeSize ]; var maximumCodeSize = Pow2[ minimumCodeSize ]; var clearCode = maximumCodeSize; var endCode = maximumCodeSize + 1; // initialise buffers var numCodes = maximumCodeSize + 2; var pCodesEnd = pCodes; for( ushort i = 0; i < numCodes; i++ ) { pIndicies[ i ] = pCodesEnd; *pCodesEnd++ = 1; *pCodesEnd++ = i; } // LZW decode loop uint previousCode = NoCode; // last code processed uint mask = (uint) ( nextSize - 1 ); // mask out code bits uint shiftRegister = 0; // shift register holds the bytes coming in from the input stream, we shift down by the number of bits int bitsAvailable = 0; // number of bits available to read in the shift register int bytesAvailable = 0; // number of bytes left in current block uint* pD = pCurBlock; // pointer to next bits in current block while( true ) { // get next code uint curCode = shiftRegister & mask; // did we read enough bits? if( bitsAvailable >= codeSize ) { // we had enough bits in the shift register so shunt it down bitsAvailable -= codeSize; shiftRegister >>= codeSize; } else { // not enough bits in register, so get more // if start of new block if( bytesAvailable <= 0 ) { // read blocksize var pBlock = &pData[ D++ ]; bytesAvailable = *pBlock++; D += bytesAvailable; // exit if end of stream if( bytesAvailable == 0 ) { return; } // copy block into buffer pCurBlock[ ( bytesAvailable - 1 ) / 4 ] = 0; // zero last entry Buffer.MemoryCopy( pBlock, pCurBlock, 256, bytesAvailable ); // reset data pointer pD = pCurBlock; } // load shift register from data pointer shiftRegister = *pD++; int newBits = bytesAvailable >= 4 ? 32 : bytesAvailable * 8; bytesAvailable -= 4; // read remaining bits if( bitsAvailable > 0 ) { var bitsRemaining = codeSize - bitsAvailable; curCode |= ( shiftRegister << bitsAvailable ) & mask; shiftRegister >>= bitsRemaining; bitsAvailable = newBits - bitsRemaining; } else { curCode = shiftRegister & mask; shiftRegister >>= codeSize; bitsAvailable = newBits - codeSize; } } // process code if( curCode == clearCode ) { // reset codes codeSize = minimumCodeSize + 1; nextSize = Pow2[ codeSize ]; numCodes = maximumCodeSize + 2; // reset buffer write pos pCodesEnd = &pCodes[ numCodes * 2 ]; // clear previous code previousCode = NoCode; mask = (uint)( nextSize - 1 ); continue; } else if( curCode == endCode ) { // stop break; } bool plusOne = false; ushort* pCodePos = null; if( curCode < numCodes ) { // write existing code pCodePos = pIndicies[ curCode ]; } else if( previousCode != NoCode ) { // write previous code pCodePos = pIndicies[ previousCode ]; plusOne = true; } else { continue; } // output colours var codeLength = *pCodePos++; var newCode = *pCodePos; var pEnd = pCodePos + codeLength; do { var code = *pCodePos++; if( code != TransparentIndex && pWrite < pImageEnd ) { *pWrite = pColourTable[ code ]; } if( ++pWrite == pRowEnd ) { pRow -= Width; pWrite = pRow; pRowEnd = pRow + ImageWidth; pImageEnd = pRow + safeWidth; if( pWrite < pOutput ) { SkipBlocks(); return; } } } while( pCodePos < pEnd ); if( plusOne ) { if( newCode != TransparentIndex && pWrite < pImageEnd ) { *pWrite = pColourTable[ newCode ]; } if( ++pWrite == pRowEnd ) { pRow -= Width; pWrite = pRow; pRowEnd = pRow + ImageWidth; pImageEnd = pRow + safeWidth; if( pWrite < pOutput ) { break; } } } // create new code if( previousCode != NoCode && numCodes != MaxCodes ) { // get previous code from buffer pCodePos = pIndicies[ previousCode ]; codeLength = *pCodePos++; // resize buffer if required (should be rare) if( pCodesEnd + codeLength + 1 >= pCodeBufferEnd ) { var pBase = pCodes; // realloc buffer CodesLength *= 2; CodesHandle = Marshal.ReAllocHGlobal( CodesHandle, (IntPtr)( CodesLength * sizeof( ushort ) ) ); pCodes = (ushort*) CodesHandle.ToPointer(); pCodeBufferEnd = pCodes + CodesLength; // rebase pointers pCodesEnd = pCodes + ( pCodesEnd - pBase ); for( int i=0; i < numCodes; i++ ) { pIndicies[ i ] = pCodes + ( pIndicies[ i ] - pBase ); } pCodePos = pIndicies[ previousCode ]; pCodePos++; } // add new code pIndicies[ numCodes++ ] = pCodesEnd; *pCodesEnd++ = (ushort)( codeLength + 1 ); // copy previous code sequence Buffer.MemoryCopy( pCodePos, pCodesEnd, codeLength * sizeof( ushort ), codeLength * sizeof( ushort ) ); pCodesEnd += codeLength; // append new code *pCodesEnd++ = newCode; } // increase code size? if( numCodes >= nextSize && codeSize < 12 ) { nextSize = Pow2[ ++codeSize ]; mask = (uint)( nextSize - 1 ); } // remember last code processed previousCode = curCode; } // consume any remaining blocks SkipBlocks(); } } } #else // dispose isn't needed for the safe implementation but keep here for interface parity public Decoder() { } public void Dispose() { Dispose( true ); } protected virtual void Dispose( bool disposing ) { } int[] Indices = new int[ 4096 ]; ushort[] Codes = new ushort[ 128 * 1024 ]; uint[] CurBlock = new uint[ 64 ]; void DecompressLZW() { // output write position int row = ( Height - ImageTop - 1 ) * Width; // reverse rows for unity texture coords int col = ImageLeft; int rightEdge = ImageLeft + ImageWidth; // setup codes int minimumCodeSize = Input[ D++ ]; if( minimumCodeSize > 11 ) { minimumCodeSize = 11; } var codeSize = minimumCodeSize + 1; var nextSize = Pow2[ codeSize ]; var maximumCodeSize = Pow2[ minimumCodeSize ]; var clearCode = maximumCodeSize; var endCode = maximumCodeSize + 1; // initialise buffers var codesEnd = 0; var numCodes = maximumCodeSize + 2; for( ushort i = 0; i < numCodes; i++ ) { Indices[ i ] = codesEnd; Codes[ codesEnd++ ] = 1; // length Codes[ codesEnd++ ] = i; // code } // LZW decode loop uint previousCode = NoCode; // last code processed uint mask = (uint) ( nextSize - 1 ); // mask out code bits uint shiftRegister = 0; // shift register holds the bytes coming in from the input stream, we shift down by the number of bits int bitsAvailable = 0; // number of bits available to read in the shift register int bytesAvailable = 0; // number of bytes left in current block int blockPos = 0; while( true ) { // get next code uint curCode = shiftRegister & mask; if( bitsAvailable >= codeSize ) { bitsAvailable -= codeSize; shiftRegister >>= codeSize; } else { // reload shift register // if start of new block if( bytesAvailable <= 0 ) { // read blocksize bytesAvailable = Input[ D++ ]; // exit if end of stream if( bytesAvailable == 0 ) { return; } // read block CurBlock[ ( bytesAvailable - 1 ) / 4 ] = 0; // zero last entry Buffer.BlockCopy( Input, D, CurBlock, 0, bytesAvailable ); blockPos = 0; D += bytesAvailable; } // load shift register shiftRegister = CurBlock[ blockPos++ ]; int newBits = bytesAvailable >= 4 ? 32 : bytesAvailable * 8; bytesAvailable -= 4; // read remaining bits if( bitsAvailable > 0 ) { var bitsRemaining = codeSize - bitsAvailable; curCode |= ( shiftRegister << bitsAvailable ) & mask; shiftRegister >>= bitsRemaining; bitsAvailable = newBits - bitsRemaining; } else { curCode = shiftRegister & mask; shiftRegister >>= codeSize; bitsAvailable = newBits - codeSize; } } // process code if( curCode == clearCode ) { // reset codes codeSize = minimumCodeSize + 1; nextSize = Pow2[ codeSize ]; numCodes = maximumCodeSize + 2; // reset buffer write pos codesEnd = numCodes * 2; // clear previous code previousCode = NoCode; mask = (uint) ( nextSize - 1 ); continue; } else if( curCode == endCode ) { // stop break; } bool plusOne = false; int codePos = 0; if( curCode < numCodes ) { // write existing code codePos = Indices[ curCode ]; } else if( previousCode != NoCode ) { // write previous code codePos = Indices[ previousCode ]; plusOne = true; } else { continue; } // output colours var codeLength = Codes[ codePos++ ]; var newCode = Codes[ codePos ]; for( int i = 0; i < codeLength; i++ ) { var code = Codes[ codePos++ ]; if( code != TransparentIndex && col < Width ) { Output[ row + col ] = ActiveColourTable[ code ]; } if( ++col == rightEdge ) { col = ImageLeft; row -= Width; if( row < 0 ) { SkipBlocks(); return; } } } if( plusOne ) { if( newCode != TransparentIndex && col < Width ) { Output[ row + col ] = ActiveColourTable[ newCode ]; } if( ++col == rightEdge ) { col = ImageLeft; row -= Width; if( row < 0 ) { break; } } } // create new code if( previousCode != NoCode && numCodes != Indices.Length ) { // get previous code from buffer codePos = Indices[ previousCode ]; codeLength = Codes[ codePos++ ]; // resize buffer if required (should be rare) if( codesEnd + codeLength + 1 >= Codes.Length ) { Array.Resize( ref Codes, Codes.Length * 2 ); } // add new code Indices[ numCodes++ ] = codesEnd; Codes[ codesEnd++ ] = (ushort) ( codeLength + 1 ); // copy previous code sequence var stop = codesEnd + codeLength; while( codesEnd < stop ) { Codes[ codesEnd++ ] = Codes[ codePos++ ]; } // append new code Codes[ codesEnd++ ] = newCode; } // increase code size? if( numCodes >= nextSize && codeSize < 12 ) { nextSize = Pow2[ ++codeSize ]; mask = (uint) ( nextSize - 1 ); } // remember last code processed previousCode = curCode; } // skip any remaining blocks SkipBlocks(); } #endif // mgGIF_UNSAFE public static string Ident() { var v = "1.1"; var e = BitConverter.IsLittleEndian ? "L" : "B"; #if ENABLE_IL2CPP var b = "N"; #else var b = "M"; #endif #if mgGIF_UNSAFE var s = "U"; #else var s = "S"; #endif #if NET_4_6 var n = "4.x"; #else var n = "2.0"; #endif return String.Format("{0} {1}{2}{3} {4}", v, e, s, b, n); } } }