//----------------------------------------------------------------------------- // Copyright 2017 Old Moat Games. All rights reserved. // // Decoder adapted from NGif by gOODiDEA.NET. // //----------------------------------------------------------------------------- using System; using System.IO; using UnityEngine; namespace OldMoatGames { public class GifDecoder { public enum Status { // File read status StatusOk, // No errors StatusFormatError, // Error decoding file (may be partially decoded) StatusOpenError // Unable to open source } public GifDecoder(bool compatibilityMode) { _compatibilityMode = compatibilityMode; } private void RewindReader() { _frameCount = 0; AllFramesRead = false; _inStream.Position = _imageDataOffset; //go to first image location in stream } private void SetPixels() { // fill in starting image contents based on last image's dispose code if (_lastDispose > 0) { //lastDispose = 1; Do not dispose. The graphic is to be left in place. //lastDispose = 2; Dispose to background. The area used by the graphic must be restored to the background color. //lastDispose = 3; Restore to previous. The decoder is required to restore the area overwritten by the graphic with what was there prior to rendering the graphic. var n = _frameCount - 1; if (n > 0) if (_lastDispose == 2) { // Restore to background color. The area used by the graphic must be restored to the background color. var fillcolor = _transparency ? 0 : _lastBgColor; for (var i = 0; i < _lih; i++) { var line = i; line += _liy; if (line >= _height) continue; var linein = _height - line - 1; var dx = linein * _width + _lix; var endx = dx + _liw; while (dx < endx) _image[dx++] = fillcolor; } } /*else if (_compatibilityMode && _lastDispose == 3) { for (var i = 0; i < _lih; i++) { var line = i; line += _liy; if (line >= _height) continue; var linein = _height - line - 1; var dx = linein * _width + _lix; var endx = dx + _liw; while (dx < endx) { _image[dx] = _prevImage[dx]; dx++; } } }*/ } // copy each source line to the appropriate place in the destination var pass = 1; var inc = 8; var iline = 0; for (var i = 0; i < _ih; i++) { var line = i; if (_interlace) { if (iline >= _ih) { pass++; switch (pass) { case 2: iline = 4; break; case 3: iline = 2; inc = 4; break; case 4: iline = 1; inc = 2; break; } } line = iline; iline += inc; } line += _iy; if (line >= _height) continue; var sx = i * _iw; // start of line in source var linein = _height - line - 1; var dx = linein * _width + _ix; var endx = dx + _iw; for (; dx < endx; dx++) { var c = _act[_pixelIndexes[sx++] & 0xff]; if (c != 0) _image[dx] = c; } } } private void DecodeImageData() { var pixelCount = _iw * _ih; int datum, bi = 0; if (_pixelIndexes == null || _pixelIndexes.Length < pixelCount) _pixelIndexes = new byte[pixelCount]; // allocate new pixel array if (_prefix == null) _prefix = new short[MaxStackSize]; if (_suffix == null) _suffix = new byte[MaxStackSize]; if (_pixelStack == null) _pixelStack = new byte[MaxStackSize + 1]; // Initialize GIF data stream decoder. var litWidth = Read(); if (litWidth < 2 || 8 < litWidth) { Debug.Log("litWidth out of range"); return; } const int maxWidth = 12; const int decoderInvalidCode = 0xffff; var width = 1 + litWidth; var clear = 1 << litWidth; var eof = clear + 1; var hi = clear + 1; var overflow = 1 << width; var last = (ushort)0xffff; var o = 0; var suffix = new byte[1 << maxWidth]; var prefix = new ushort[1 << maxWidth]; var meaningfulBitsInDatum = 0; var bytesToExtract = 0; var maxCode = (1 << width) - 1; datum = 0; while (true) { if (meaningfulBitsInDatum < width) { if (bytesToExtract == 0) { bytesToExtract = ReadBlock(); if (bytesToExtract <= 0) break; bi = 0; } var newDatum = _block[bi] << meaningfulBitsInDatum; datum += newDatum; meaningfulBitsInDatum += 8; bi++; bytesToExtract--; continue; } var code = (ushort)(datum & maxCode); datum >>= width; meaningfulBitsInDatum -= width; if (code < clear) { _pixelIndexes[o] = (byte)code; o++; if (last != decoderInvalidCode) { suffix[hi] = (byte)code; prefix[hi] = last; } } else if (code == clear) { width = 1 + litWidth; maxCode = (1 << width) - 1; hi = eof; overflow = 1 << width; last = decoderInvalidCode; continue; } else if (code == eof) { break; } else if (code <= hi) { var c = code; var i = _pixelIndexes.Length - 1; if (code == hi && last != decoderInvalidCode) { c = last; while (c >= clear) c = prefix[c]; _pixelIndexes[i] = (byte)c; i--; c = last; } while (c >= clear) { _pixelIndexes[i] = suffix[c]; i--; c = prefix[c]; } _pixelIndexes[i] = (byte)c; var m = Math.Min(_pixelIndexes.Length - o, _pixelIndexes.Length - i); Buffer.BlockCopy(_pixelIndexes, i, _pixelIndexes, o, m); o += m; if (last != decoderInvalidCode) { suffix[hi] = (byte)c; prefix[hi] = last; } } else { Debug.Log("Invalid code"); break; } last = code; hi = hi + 1; if (hi != overflow) continue; if (width == maxWidth) { last = decoderInvalidCode; hi--; } else { width++; maxCode = (1 << width) - 1; overflow <<= 1; } } } /** * Initializes or re-initializes reader */ private void Init() { _status = Status.StatusOk; _frameCount = 0; _currentFrame = null; AllFramesRead = false; _gct = null; _lct = null; } /** * Reads a single byte from the input stream. */ private int Read() { var curByte = 0; try { curByte = _inStream.ReadByte(); } catch (IOException) { _status = Status.StatusFormatError; } return curByte; } /** * Reads next variable length block from input. * * @return number of bytes stored in "buffer" */ private int ReadBlock() { _blockSize = Read(); var n = 0; if (_blockSize <= 0) return n; try { while (n < _blockSize) { var count = _inStream.Read(_block, n, _blockSize - n); if (count == -1) break; n += count; } } catch (IOException ex) { Debug.Log("exception " + ex); } if (n < _blockSize) _status = Status.StatusFormatError; return n; } /** * Reads color table as 256 RGB integer values * * @param ncolors int number of colors to read * @return int array containing 256 colors (packed ARGB with full alpha) */ private int[] ReadColorTable(int ncolors) { var nbytes = 3 * ncolors; int[] tab = null; var c = new byte[nbytes]; var n = 0; try { n = _inStream.Read(c, 0, c.Length); } catch (IOException) { } if (n < nbytes) { _status = Status.StatusFormatError; } else { tab = new int[256]; // max size to avoid bounds checks var i = 0; var j = 0; while (i < ncolors) { uint r = c[j++]; var g = c[j++] & (uint)0xff; var b = c[j++] & (uint)0xff; tab[i++] = (int)(0xff000000 | (b << 16) | (g << 8) | r); } } return tab; } /** * Reads Graphics Control Extension values */ private void ReadGraphicControlExt() { Read(); // block size var packed = Read(); // packed fields _dispose = (packed & 0x1c) >> 2; // disposal method if (_dispose == 0) _dispose = 1; // elect to keep old image if discretionary _transparency = (packed & 1) != 0; _delay = ReadShort() / 100f; // delay in seconds _transIndex = Read(); // transparent color index Read(); // block terminator } /** * Reads GIF file header information. */ private void ReadHeader() { var id = ""; for (var i = 0; i < 6; i++) id += (char)Read(); if (!id.StartsWith("GIF")) { _status = Status.StatusFormatError; return; } ReadLsd(); if (_gctFlag && !Error()) { _gct = ReadColorTable(_gctSize); _bgColor = _gct[_bgIndex]; } _imageDataOffset = _inStream.Position; } /** * Reads next frame image */ private void ReadImage() { _ix = ReadShort(); // (sub)image position & size _iy = ReadShort(); _iw = ReadShort(); _ih = ReadShort(); var packed = Read(); _lctFlag = (packed & 0x80) != 0; // 1 - local color table flag _interlace = (packed & 0x40) != 0; // 2 - interlace flag // 3 - sort flag // 4-5 - reserved _lctSize = 2 << (packed & 7); // 6-8 - local color table size if (_lctFlag) { _lct = ReadColorTable(_lctSize); // read table _act = _lct; // make local table active } else { _act = _gct; // make global table active if (_bgIndex == _transIndex) _bgColor = 0; } var save = 0; if (_transparency) { save = _act[_transIndex]; _act[_transIndex] = 0; // set transparent color if specified } if (_act == null) _status = Status.StatusFormatError; // no color table defined if (Error()) return; DecodeImageData(); // decode pixel data Skip(); if (Error()) return; // create new image to receive frame data if (_image == null) _image = new int[_width * _height]; if (_compatibilityMode && _prevImage == null) _prevImage = new int[_width * _height]; if (_bimage == null) _bimage = new byte[_width * _height * sizeof(int)]; if (_compatibilityMode && _lastDispose != 3) Buffer.BlockCopy(_image, 0, _prevImage, 0, _image.Length * sizeof(int)); // keep a copy of the image in compatibility mode SetPixels(); // transfer pixel data to image //copy image Buffer.BlockCopy(_image, 0, _bimage, 0, _bimage.Length); if (_compatibilityMode && _dispose == 3) Buffer.BlockCopy(_prevImage, 0, _image, 0, _prevImage.Length * sizeof(int)); // keep a copy of the image in compatibility mode _currentFrame = new GifFrame(_bimage, _delay); //save image as current image _frameCount++; if (_transparency) _act[_transIndex] = save; ResetFrame(); } /** * Reads Logical Screen Descriptor */ private void ReadLsd() { // logical screen size _width = ReadShort(); _height = ReadShort(); // packed fields var packed = Read(); _gctFlag = (packed & 0x80) != 0; // 1 : global color table flag // 2-4 : color resolution // 5 : gct sort flag _gctSize = 2 << (packed & 7); // 6-8 : gct size _bgIndex = Read(); // background color index //_pixelAspect = Read(); // pixel aspect ratio Read(); // pixel aspect ratio } /** * Reads Netscape extenstion to obtain iteration count */ private void ReadNetscapeExt() { do { ReadBlock(); if (_block[0] != 1) continue; // loop count sub-block var b1 = _block[1] & 0xff; var b2 = _block[2] & 0xff; _loopCount = (b2 << 8) | b1; } while (_blockSize > 0 && !Error()); } /** * Reads next 16-bit value, LSB first */ private int ReadShort() { // read 16-bit value, LSB first return Read() | (Read() << 8); } /** * Resets frame state for reading next image. */ private void ResetFrame() { _lastDispose = _dispose; _lix = _ix; _liy = _iy; _liw = _iw; _lih = _ih; _lastBgColor = _bgColor; _lct = null; } /** * Skips variable length blocks up to and including * next zero length block. */ private void Skip() { do { ReadBlock(); } while (_blockSize > 0 && !Error()); } /// /// Holds frame data and frame delay /// public class GifFrame { /// /// Time in seconds till next frame /// public float Delay; /// /// Frame data /// public byte[] Image; public GifFrame(byte[] im, float del) { Image = im; Delay = del; } } #region Public Fields /// /// Number of frames in GIF. Value is 0 as long as the last frame has not been reached /// public int NumberOfFrames { get; private set; } /// /// Set to true when all frames have been read /// public bool AllFramesRead { get; private set; } /// /// #endregion #region Internal fields private Stream _inStream; // Data stream holding the GIF data private Status _status; // Status of the decoder private int _width; // full image width private int _height; // full image height private bool _gctFlag; // global color table used private int _gctSize; // size of global color table private int _loopCount = 1; // iterations; 0 = repeat forever private int[] _gct; // global color table private int[] _lct; // local color table private int[] _act; // active color table private int _bgIndex; // background color index private int _bgColor; // background color private int _lastBgColor; // previous bg color //private int _pixelAspect; // pixel aspect ratio private bool _lctFlag; // local color table flag private bool _interlace; // interlace flag private int _lctSize; // local color table size private int _ix, _iy, _iw, _ih; // current image rectangle private int _lix, _liy, _liw, _lih; // last image rect private int[] _image; // current frame private byte[] _bimage; // current frame private readonly byte[] _block = new byte[256]; // current data block private int _blockSize; // block size private readonly bool _compatibilityMode; // when set to true all disposal methods are supported private int[] _prevImage; // previous image. Used in compatibility mode private int _dispose; // last graphic control extension info. 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev private int _lastDispose; private bool _transparency; // use transparent color private float _delay; // delay in milliseconds private int _transIndex; // transparent color index private long _imageDataOffset; // start of image data in stream private const int MaxStackSize = 4096; // max decoder pixel stack size private const int NullCode = -1; // max decoder pixel stack size // LZW decoder working arrays private short[] _prefix; private byte[] _suffix; private byte[] _pixelStack; private byte[] _pixelIndexes; //protected ArrayList frames; // frames read from current file //protected bool CacheFrames; private GifFrame _currentFrame; private int _frameCount; #endregion #region Public API /** * Gets display duration for specified frame. * * @param n int index of frame * @return delay in milliseconds */ public float GetDelayCurrentFrame() { return _currentFrame.Delay; } /** * Gets the number of frames read from file. * @return frame count. Only those frames that are decoded are counted */ public int GetFrameCount() { return _frameCount; } /** * Gets the "Netscape" iteration count, if any. * A count of 0 means repeat indefinitely. * * @return iteration count if one was specified, else 1. */ public int GetLoopCount() { return _loopCount; } /** * Gets the next image frame * * @return BufferedImage representation of frame, or null if n is invalid. */ public GifFrame GetCurrentFrame() { return _currentFrame; } /** * Gets image width. * * @return GIF image width */ public int GetFrameWidth() { return _width; } /** * Gets image height. * * @return GIF image height */ public int GetFrameHeight() { return _height; } /** * Reads GIF image from stream * * @param BufferedInputStream containing GIF file. * @return read status code (0 = no errors) */ public Status Read(Stream inStream) { Init(); if (inStream != null) { _inStream = inStream; ReadHeader(); if (Error()) _status = Status.StatusFormatError; } else { _status = Status.StatusOpenError; } return _status; } /** * Reset reading of the GIF */ public void Reset() { _inStream.Position = 0; Read(_inStream); } /** * Close the stream */ public void Close() { _inStream.Dispose(); } /** * Returns true if an error was encountered during reading/decoding */ private bool Error() { return _status != Status.StatusOk; } /** * Reads the next frame */ public void ReadNextFrame(bool loop) { // read GIF file content blocks while (!Error()) { var code = Read(); switch (code) { case 0x2C: // image separator ReadImage(); //if (readSingleFrame) return; return; case 0x21: // extension code = Read(); switch (code) { case 0xf9: // graphics control extension ReadGraphicControlExt(); break; case 0xff: // application extension ReadBlock(); var app = ""; for (var i = 0; i < 11; i++) app += (char)_block[i]; if (app.Equals("NETSCAPE2.0")) ReadNetscapeExt(); else Skip(); // don't care break; default: // uninteresting extension Skip(); break; } break; case 0x3b: // terminator //we read all frames NumberOfFrames = _frameCount; if (loop) { RewindReader(); break; } AllFramesRead = true; return; case 0x00: // bad byte, but keep going and see what happens break; default: _status = Status.StatusFormatError; break; } } } #endregion } }