// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer // as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer) // Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) // Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved. // The rest of the source file is licensed under MIT License. // Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; using Avalonia; using Avalonia.Media.Imaging; using static Avalonia.Gif.Extensions.StreamExtensions; namespace Avalonia.Gif.Decoding { public sealed class GifDecoder : IDisposable { private static readonly ReadOnlyMemory G87AMagic = "GIF87a"u8.ToArray().AsMemory(); private static readonly ReadOnlyMemory G89AMagic = "GIF89a"u8.ToArray().AsMemory(); private static readonly ReadOnlyMemory NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory(); private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10); private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100); private static readonly GifColor TransparentColor = new(0, 0, 0, 0); private static readonly int MaxTempBuf = 768; private static readonly int MaxStackSize = 4096; private static readonly int MaxBits = 4097; private readonly Stream _fileStream; private readonly CancellationToken _currentCtsToken; private readonly bool _hasFrameBackups; private int _gctSize, _bgIndex, _prevFrame = -1, _backupFrame = -1; private bool _gctUsed; private GifRect _gifDimensions; // private ulong _globalColorTable; private readonly int _backBufferBytes; private GifColor[] _bitmapBackBuffer; private short[] _prefixBuf; private byte[] _suffixBuf; private byte[] _pixelStack; private byte[] _indexBuf; private byte[] _backupFrameIndexBuf; private volatile bool _hasNewFrame; public GifHeader Header { get; private set; } public readonly List Frames = new(); public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height); public GifDecoder(Stream fileStream, CancellationToken currentCtsToken) { _fileStream = fileStream; _currentCtsToken = currentCtsToken; ProcessHeaderData(); ProcessFrameData(); Header.IterationCount = Header.Iterations switch { -1 => new GifRepeatBehavior { Count = 1 }, 0 => new GifRepeatBehavior { LoopForever = true }, > 0 => new GifRepeatBehavior { Count = Header.Iterations }, _ => Header.IterationCount }; var pixelCount = _gifDimensions.TotalPixels; _hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore); _bitmapBackBuffer = new GifColor[pixelCount]; _indexBuf = new byte[pixelCount]; if (_hasFrameBackups) _backupFrameIndexBuf = new byte[pixelCount]; _prefixBuf = new short[MaxStackSize]; _suffixBuf = new byte[MaxStackSize]; _pixelStack = new byte[MaxStackSize + 1]; _backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor)); } public void Dispose() { Frames.Clear(); _bitmapBackBuffer = null; _prefixBuf = null; _suffixBuf = null; _pixelStack = null; _indexBuf = null; _backupFrameIndexBuf = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int PixCoord(int x, int y) => x + y * _gifDimensions.Width; static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) }; private void ClearImage() { Array.Fill(_bitmapBackBuffer, TransparentColor); //ClearArea(_gifDimensions); _prevFrame = -1; _backupFrame = -1; } public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false) { if (_currentCtsToken.IsCancellationRequested) return; if (fIndex < 0 | fIndex >= Frames.Count) return; if (_prevFrame == fIndex) return; if (fIndex == 0 || forceClear || fIndex < _prevFrame) ClearImage(); DisposePreviousFrame(); _prevFrame++; // render intermediate frame for (int idx = _prevFrame; idx < fIndex; ++idx) { var prevFrame = Frames[idx]; if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore) continue; if (prevFrame.FrameDisposalMethod == FrameDisposal.Background) { ClearArea(prevFrame.Dimensions); continue; } RenderFrameAt(idx, writeableBitmap); } RenderFrameAt(fIndex, writeableBitmap); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap) { var tmpB = ArrayPool.Shared.Rent(MaxTempBuf); var curFrame = Frames[idx]; DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB); if (_hasFrameBackups & curFrame.ShouldBackup) { Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels); _backupFrame = idx; } DrawFrame(curFrame, _indexBuf); _prevFrame = idx; _hasNewFrame = true; using var lockedBitmap = writeableBitmap.Lock(); WriteBackBufToFb(lockedBitmap.Address); ArrayPool.Shared.Return(tmpB); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawFrame(GifFrame curFrame, Memory frameIndexSpan) { var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable; var cX = curFrame.Dimensions.X; var cY = curFrame.Dimensions.Y; var cH = curFrame.Dimensions.Height; var cW = curFrame.Dimensions.Width; var tC = curFrame.TransparentColorIndex; var hT = curFrame.HasTransparency; if (curFrame.IsInterlaced) { for (var i = 0; i < 4; i++) { var curPass = Pass[i]; var y = curPass.Start; while (y < cH) { DrawRow(y); y += curPass.Step; } } } else { for (var i = 0; i < cH; i++) DrawRow(i); } //for (var row = 0; row < cH; row++) void DrawRow(int row) { // Get the starting point of the current row on frame's index stream. var indexOffset = row * cW; // Get the target backbuffer offset from the frames coords. var targetOffset = PixCoord(cX, row + cY); var len = _bitmapBackBuffer.Length; for (var i = 0; i < cW; i++) { var indexColor = frameIndexSpan.Span[indexOffset + i]; if (activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length) return; if (!(hT & indexColor == tC)) _bitmapBackBuffer[targetOffset] = activeColorTable[indexColor]; targetOffset++; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DisposePreviousFrame() { if (_prevFrame == -1) return; var prevFrame = Frames[_prevFrame]; switch (prevFrame.FrameDisposalMethod) { case FrameDisposal.Background: ClearArea(prevFrame.Dimensions); break; case FrameDisposal.Restore: if (_hasFrameBackups && _backupFrame != -1) DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf); else ClearArea(prevFrame.Dimensions); break; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearArea(GifRect area) { for (var y = 0; y < area.Height; y++) { var targetOffset = PixCoord(area.X, y + area.Y); for (var x = 0; x < area.Width; x++) _bitmapBackBuffer[targetOffset + x] = TransparentColor; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span indexSpan, byte[] tempBuf) { _fileStream.Position = curFrame.LzwStreamPosition; var totalPixels = curFrame.Dimensions.TotalPixels; // Initialize GIF data stream decoder. var dataSize = curFrame.LzwMinCodeSize; var clear = 1 << dataSize; var endOfInformation = clear + 1; var available = clear + 2; var oldCode = -1; var codeSize = dataSize + 1; var codeMask = (1 << codeSize) - 1; for (var code = 0; code < clear; code++) { _prefixBuf[code] = 0; _suffixBuf[code] = (byte)code; } // Decode GIF pixel stream. int bits, first, top, pixelIndex; var datum = bits = first = top = pixelIndex = 0; while (pixelIndex < totalPixels) { var blockSize = _fileStream.ReadBlock(tempBuf); if (blockSize == 0) break; var blockPos = 0; while (blockPos < blockSize) { datum += tempBuf[blockPos] << bits; blockPos++; bits += 8; while (bits >= codeSize) { // Get the next code. var code = datum & codeMask; datum >>= codeSize; bits -= codeSize; // Interpret the code if (code == clear) { // Reset decoder. codeSize = dataSize + 1; codeMask = (1 << codeSize) - 1; available = clear + 2; oldCode = -1; continue; } // Check for explicit end-of-stream if (code == endOfInformation) return; if (oldCode == -1) { indexSpan[pixelIndex++] = _suffixBuf[code]; oldCode = code; first = code; continue; } var inCode = code; if (code >= available) { _pixelStack[top++] = (byte)first; code = oldCode; if (top == MaxBits) ThrowException(); } while (code >= clear) { if (code >= MaxBits || code == _prefixBuf[code]) ThrowException(); _pixelStack[top++] = _suffixBuf[code]; code = _prefixBuf[code]; if (top == MaxBits) ThrowException(); } first = _suffixBuf[code]; _pixelStack[top++] = (byte)first; // Add new code to the dictionary if (available < MaxStackSize) { _prefixBuf[available] = (short)oldCode; _suffixBuf[available] = (byte)first; available++; if ((available & codeMask) == 0 && available < MaxStackSize) { codeSize++; codeMask += available; } } oldCode = inCode; // Drain the pixel stack. do { indexSpan[pixelIndex++] = _pixelStack[--top]; } while (top > 0); } } } while (pixelIndex < totalPixels) indexSpan[pixelIndex++] = 0; // clear missing pixels void ThrowException() => throw new LzwDecompressionException(); } /// /// Directly copies the struct array to a bitmap IntPtr. /// private void WriteBackBufToFb(IntPtr targetPointer) { if (_currentCtsToken.IsCancellationRequested) return; if (!(_hasNewFrame & _bitmapBackBuffer != null)) return; unsafe { fixed (void* src = &_bitmapBackBuffer[0]) Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes); _hasNewFrame = false; } } /// /// Processes GIF Header. /// private void ProcessHeaderData() { var str = _fileStream; var tmpB = ArrayPool.Shared.Rent(MaxTempBuf); var tempBuf = tmpB.AsSpan(); var _ = str.Read(tmpB, 0, 6); if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span)) throw new InvalidGifStreamException("Not a GIF stream."); if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span))) throw new InvalidGifStreamException( "Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray()) ); ProcessScreenDescriptor(tmpB); Header = new GifHeader { Dimensions = _gifDimensions, HasGlobalColorTable = _gctUsed, // GlobalColorTableCacheID = _globalColorTable, GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize), GlobalColorTableSize = _gctSize, BackgroundColorIndex = _bgIndex, HeaderSize = _fileStream.Position }; ArrayPool.Shared.Return(tmpB); } /// /// Parses colors from file stream to target color table. /// private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors) { var nBytes = 3 * nColors; var target = new GifColor[nColors]; var n = stream.Read(rawBufSpan, 0, nBytes); if (n < nBytes) throw new InvalidOperationException("Wrong color table bytes."); int i = 0, j = 0; while (i < nColors) { var r = rawBufSpan[j++]; var g = rawBufSpan[j++]; var b = rawBufSpan[j++]; target[i++] = new GifColor(r, g, b); } return target; } /// /// Parses screen and other GIF descriptors. /// private void ProcessScreenDescriptor(byte[] tempBuf) { var width = _fileStream.ReadUShortS(tempBuf); var height = _fileStream.ReadUShortS(tempBuf); var packed = _fileStream.ReadByteS(tempBuf); _gctUsed = (packed & 0x80) != 0; _gctSize = 2 << (packed & 7); _bgIndex = _fileStream.ReadByteS(tempBuf); _gifDimensions = new GifRect(0, 0, width, height); _fileStream.Skip(1); } /// /// Parses all frame data. /// private void ProcessFrameData() { _fileStream.Position = Header.HeaderSize; var tempBuf = ArrayPool.Shared.Rent(MaxTempBuf); var terminate = false; var curFrame = 0; Frames.Add(new GifFrame()); do { var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf); switch (blockType) { case BlockTypes.Empty: break; case BlockTypes.Extension: ProcessExtensions(ref curFrame, tempBuf); break; case BlockTypes.ImageDescriptor: ProcessImageDescriptor(ref curFrame, tempBuf); _fileStream.SkipBlocks(tempBuf); break; case BlockTypes.Trailer: Frames.RemoveAt(Frames.Count - 1); terminate = true; break; default: _fileStream.SkipBlocks(tempBuf); break; } // Break the loop when the stream is not valid anymore. if (_fileStream.Position >= _fileStream.Length & terminate == false) throw new InvalidProgramException("Reach the end of the filestream without trailer block."); } while (!terminate); ArrayPool.Shared.Return(tempBuf); } /// /// Parses GIF Image Descriptor Block. /// private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf) { var str = _fileStream; var currentFrame = Frames[curFrame]; // Parse frame dimensions. var frameX = str.ReadUShortS(tempBuf); var frameY = str.ReadUShortS(tempBuf); var frameW = str.ReadUShortS(tempBuf); var frameH = str.ReadUShortS(tempBuf); frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX); frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY); currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH); // Unpack interlace and lct info. var packed = str.ReadByteS(tempBuf); currentFrame.IsInterlaced = (packed & 0x40) != 0; currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0; currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1); if (currentFrame.IsLocalColorTableUsed) currentFrame.LocalColorTable = ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize); currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf); currentFrame.LzwStreamPosition = str.Position; curFrame += 1; Frames.Add(new GifFrame()); } /// /// Parses GIF Extension Blocks. /// private void ProcessExtensions(ref int curFrame, byte[] tempBuf) { var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf); switch (extType) { case ExtensionType.GraphicsControl: _fileStream.ReadBlock(tempBuf); var currentFrame = Frames[curFrame]; var packed = tempBuf[0]; currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2); if ( currentFrame.FrameDisposalMethod != FrameDisposal.Restore && currentFrame.FrameDisposalMethod != FrameDisposal.Background ) currentFrame.ShouldBackup = true; currentFrame.HasTransparency = (packed & 1) != 0; currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10); if (currentFrame.FrameDelay <= FrameDelayThreshold) currentFrame.FrameDelay = FrameDelayDefault; currentFrame.TransparentColorIndex = tempBuf[3]; break; case ExtensionType.Application: var blockLen = _fileStream.ReadBlock(tempBuf); var _ = tempBuf.AsSpan(0, blockLen); var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length); if (blockHeader.SequenceEqual(NetscapeMagic.Span)) { var count = 1; while (count > 0) count = _fileStream.ReadBlock(tempBuf); var iterationCount = SpanToShort(tempBuf.AsSpan(1)); Header.Iterations = iterationCount; } else _fileStream.SkipBlocks(tempBuf); break; default: _fileStream.SkipBlocks(tempBuf); break; } } } }