mirror of https://github.com/leiurayer/downkyi
parent
66e73a2537
commit
fb7cca855e
@ -0,0 +1,10 @@
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
internal enum BlockTypes
|
||||
{
|
||||
Empty = 0,
|
||||
Extension = 0x21,
|
||||
ImageDescriptor = 0x2C,
|
||||
Trailer = 0x3B,
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
internal enum ExtensionType
|
||||
{
|
||||
GraphicsControl = 0xF9,
|
||||
Application = 0xFF
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
public enum FrameDisposal
|
||||
{
|
||||
Unknown = 0,
|
||||
Leave = 1,
|
||||
Background = 2,
|
||||
Restore = 3
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Avalonia.Gif
|
||||
{
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public readonly struct GifColor
|
||||
{
|
||||
[FieldOffset(3)]
|
||||
public readonly byte A;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public readonly byte R;
|
||||
|
||||
[FieldOffset(1)]
|
||||
public readonly byte G;
|
||||
|
||||
[FieldOffset(0)]
|
||||
public readonly byte B;
|
||||
|
||||
/// <summary>
|
||||
/// A struct that represents a ARGB color and is aligned as
|
||||
/// a BGRA bytefield in memory.
|
||||
/// </summary>
|
||||
/// <param name="r">Red</param>
|
||||
/// <param name="g">Green</param>
|
||||
/// <param name="b">Blue</param>
|
||||
/// <param name="a">Alpha</param>
|
||||
public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue)
|
||||
{
|
||||
A = a;
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,657 @@
|
||||
// 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 Avalonia.Media.Imaging;
|
||||
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 static Avalonia.Gif.Extensions.StreamExtensions;
|
||||
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
public sealed class GifDecoder : IDisposable
|
||||
{
|
||||
private static readonly ReadOnlyMemory<byte> G87AMagic
|
||||
= "GIF87a"u8.ToArray().AsMemory();
|
||||
|
||||
private static readonly ReadOnlyMemory<byte> G89AMagic
|
||||
= "GIF89a"u8.ToArray().AsMemory();
|
||||
|
||||
private static readonly ReadOnlyMemory<byte> 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<GifFrame> 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<byte>.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<byte>.Shared.Return(tmpB);
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void DrawFrame(GifFrame curFrame, Memory<byte> 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<byte> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directly copies the <see cref="GifColor"/> struct array to a bitmap IntPtr.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes GIF Header.
|
||||
/// </summary>
|
||||
private void ProcessHeaderData()
|
||||
{
|
||||
var str = _fileStream;
|
||||
var tmpB = ArrayPool<byte>.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<byte>.Shared.Return(tmpB);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses colors from file stream to target color table.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses screen and other GIF descriptors.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses all frame data.
|
||||
/// </summary>
|
||||
private void ProcessFrameData()
|
||||
{
|
||||
_fileStream.Position = Header.HeaderSize;
|
||||
|
||||
var tempBuf = ArrayPool<byte>.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<byte>.Shared.Return(tempBuf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses GIF Image Descriptor Block.
|
||||
/// </summary>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses GIF Extension Blocks.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
public class GifFrame
|
||||
{
|
||||
public bool HasTransparency, IsInterlaced, IsLocalColorTableUsed;
|
||||
public byte TransparentColorIndex;
|
||||
public int LzwMinCodeSize, LocalColorTableSize;
|
||||
public long LzwStreamPosition;
|
||||
public TimeSpan FrameDelay;
|
||||
public FrameDisposal FrameDisposalMethod;
|
||||
public bool ShouldBackup;
|
||||
public GifRect Dimensions;
|
||||
public GifColor[] LocalColorTable;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Licensed under the MIT License.
|
||||
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
|
||||
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
public class GifHeader
|
||||
{
|
||||
public bool HasGlobalColorTable;
|
||||
public int GlobalColorTableSize;
|
||||
public ulong GlobalColorTableCacheId;
|
||||
public int BackgroundColorIndex;
|
||||
public long HeaderSize;
|
||||
internal int Iterations = -1;
|
||||
public GifRepeatBehavior IterationCount;
|
||||
public GifRect Dimensions;
|
||||
private GifColor[] _globarColorTable;
|
||||
public GifColor[] GlobarColorTable;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
public readonly struct GifRect
|
||||
{
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public int TotalPixels { get; }
|
||||
|
||||
public GifRect(int x, int y, int width, int height)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
TotalPixels = width * height;
|
||||
}
|
||||
|
||||
public static bool operator ==(GifRect a, GifRect b)
|
||||
{
|
||||
return a.X == b.X &&
|
||||
a.Y == b.Y &&
|
||||
a.Width == b.Width &&
|
||||
a.Height == b.Height;
|
||||
}
|
||||
public static bool operator !=(GifRect a, GifRect b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj == null || GetType() != obj.GetType())
|
||||
return false;
|
||||
|
||||
return this == (GifRect)obj;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// Licensed under the MIT License.
|
||||
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
[Serializable]
|
||||
public class InvalidGifStreamException : Exception
|
||||
{
|
||||
public InvalidGifStreamException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidGifStreamException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidGifStreamException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// Licensed under the MIT License.
|
||||
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
|
||||
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Avalonia.Gif.Decoding
|
||||
{
|
||||
[Serializable]
|
||||
public class LzwDecompressionException : Exception
|
||||
{
|
||||
public LzwDecompressionException()
|
||||
{
|
||||
}
|
||||
|
||||
public LzwDecompressionException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public LzwDecompressionException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected LzwDecompressionException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Avalonia.Gif.Extensions
|
||||
{
|
||||
[DebuggerStepThrough]
|
||||
internal static class StreamExtensions
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort SpanToShort(Span<byte> b) => (ushort)(b[0] | (b[1] << 8));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Skip(this Stream stream, long count)
|
||||
{
|
||||
stream.Position += count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a Gif block from stream while advancing the position.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ReadBlock(this Stream stream, byte[] tempBuf)
|
||||
{
|
||||
stream.Read(tempBuf, 0, 1);
|
||||
|
||||
var blockLength = (int)tempBuf[0];
|
||||
|
||||
if (blockLength > 0)
|
||||
stream.Read(tempBuf, 0, blockLength);
|
||||
|
||||
// Guard against infinite loop.
|
||||
if (stream.Position >= stream.Length)
|
||||
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block.");
|
||||
|
||||
return blockLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips GIF blocks until it encounters an empty block.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SkipBlocks(this Stream stream, byte[] tempBuf)
|
||||
{
|
||||
int blockLength;
|
||||
do
|
||||
{
|
||||
stream.Read(tempBuf, 0, 1);
|
||||
|
||||
blockLength = tempBuf[0];
|
||||
stream.Position += blockLength;
|
||||
|
||||
// Guard against infinite loop.
|
||||
if (stream.Position >= stream.Length)
|
||||
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block.");
|
||||
|
||||
} while (blockLength > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ushort ReadUShortS(this Stream stream, byte[] tempBuf)
|
||||
{
|
||||
stream.Read(tempBuf, 0, 2);
|
||||
return SpanToShort(tempBuf);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte ReadByteS(this Stream stream, byte[] tempBuf)
|
||||
{
|
||||
stream.Read(tempBuf, 0, 1);
|
||||
var finalVal = tempBuf[0];
|
||||
return finalVal;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
using Avalonia.Animation;
|
||||
using Avalonia.Gif.Decoding;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Avalonia.Gif
|
||||
{
|
||||
public class GifInstance : IDisposable
|
||||
{
|
||||
public IterationCount IterationCount { get; set; }
|
||||
public bool AutoStart { get; private set; } = true;
|
||||
private readonly GifDecoder _gifDecoder;
|
||||
private readonly WriteableBitmap? _targetBitmap;
|
||||
private TimeSpan _totalTime;
|
||||
private readonly List<TimeSpan> _frameTimes;
|
||||
private uint _iterationCount;
|
||||
private int _currentFrameIndex;
|
||||
private readonly List<ulong> _colorTableIdList;
|
||||
|
||||
public CancellationTokenSource CurrentCts { get; }
|
||||
|
||||
internal GifInstance(object newValue) : this(newValue switch
|
||||
{
|
||||
Stream s => s,
|
||||
Uri u => GetStreamFromUri(u),
|
||||
string str => GetStreamFromString(str),
|
||||
_ => throw new InvalidDataException("Unsupported source object")
|
||||
})
|
||||
{ }
|
||||
|
||||
public GifInstance(string uri) : this(GetStreamFromString(uri))
|
||||
{ }
|
||||
|
||||
public GifInstance(Uri uri) : this(GetStreamFromUri(uri))
|
||||
{ }
|
||||
|
||||
public GifInstance(Stream currentStream)
|
||||
{
|
||||
if (!currentStream.CanSeek)
|
||||
throw new InvalidDataException("The provided stream is not seekable.");
|
||||
|
||||
if (!currentStream.CanRead)
|
||||
throw new InvalidOperationException("Can't read the stream provided.");
|
||||
|
||||
currentStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
CurrentCts = new CancellationTokenSource();
|
||||
|
||||
_gifDecoder = new GifDecoder(currentStream, CurrentCts.Token);
|
||||
var pixSize = new PixelSize(_gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height);
|
||||
|
||||
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
|
||||
GifPixelSize = pixSize;
|
||||
|
||||
_totalTime = TimeSpan.Zero;
|
||||
|
||||
_frameTimes = _gifDecoder.Frames.Select(frame =>
|
||||
{
|
||||
_totalTime = _totalTime.Add(frame.FrameDelay);
|
||||
return _totalTime;
|
||||
}).ToList();
|
||||
|
||||
_gifDecoder.RenderFrame(0, _targetBitmap);
|
||||
}
|
||||
|
||||
private static Stream GetStreamFromString(string str)
|
||||
{
|
||||
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res))
|
||||
{
|
||||
throw new InvalidCastException("The string provided can't be converted to URI.");
|
||||
}
|
||||
|
||||
return GetStreamFromUri(res);
|
||||
}
|
||||
|
||||
private static Stream GetStreamFromUri(Uri uri)
|
||||
{
|
||||
var uriString = uri.OriginalString.Trim();
|
||||
|
||||
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares"))
|
||||
throw new InvalidDataException(
|
||||
"The URI provided is not currently supported.");
|
||||
|
||||
return AssetLoader.Open(uri);
|
||||
}
|
||||
|
||||
public int GifFrameCount => _frameTimes.Count;
|
||||
|
||||
public PixelSize GifPixelSize { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
CurrentCts.Cancel();
|
||||
_targetBitmap?.Dispose();
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
|
||||
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed)
|
||||
{
|
||||
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (CurrentCts.IsCancellationRequested || _targetBitmap is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var elapsedTicks = stopwatchElapsed.Ticks;
|
||||
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks);
|
||||
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x);
|
||||
var currentFrame = _frameTimes.IndexOf(targetFrame);
|
||||
if (currentFrame == -1) currentFrame = 0;
|
||||
|
||||
if (_currentFrameIndex == currentFrame)
|
||||
return _targetBitmap;
|
||||
|
||||
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks);
|
||||
|
||||
return ProcessFrameIndex(currentFrame);
|
||||
}
|
||||
|
||||
internal WriteableBitmap ProcessFrameIndex(int frameIndex)
|
||||
{
|
||||
_gifDecoder.RenderFrame(frameIndex, _targetBitmap);
|
||||
_currentFrameIndex = frameIndex;
|
||||
|
||||
return _targetBitmap;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Avalonia.Gif
|
||||
{
|
||||
[Serializable]
|
||||
internal class InvalidGifStreamException : Exception
|
||||
{
|
||||
public InvalidGifStreamException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidGifStreamException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidGifStreamException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
Reference in new issue