GifDecoder.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. // This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer
  2. // as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer)
  3. // Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
  4. // Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved.
  5. // The rest of the source file is licensed under MIT License.
  6. // Copyright (C) 2018 Jumar A. Macato, All Rights Reserved.
  7. using System.Buffers;
  8. using System.Runtime.CompilerServices;
  9. using System.Runtime.InteropServices;
  10. using System.Text;
  11. using Avalonia;
  12. using Avalonia.Media.Imaging;
  13. using static PicView.Avalonia.AnimatedImage.Extensions.StreamExtensions;
  14. namespace PicView.Avalonia.AnimatedImage.Decoding;
  15. public sealed class GifDecoder : IDisposable
  16. {
  17. private static readonly ReadOnlyMemory<byte> G87AMagic
  18. = "GIF87a"u8.ToArray().AsMemory();
  19. private static readonly ReadOnlyMemory<byte> G89AMagic
  20. = "GIF89a"u8.ToArray().AsMemory();
  21. private static readonly ReadOnlyMemory<byte> NetscapeMagic
  22. = "NETSCAPE2.0"u8.ToArray().AsMemory();
  23. private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10);
  24. private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100);
  25. private static readonly GifColor TransparentColor = new(0, 0, 0, 0);
  26. private static readonly int MaxTempBuf = 768;
  27. private static readonly int MaxStackSize = 4096;
  28. private static readonly int MaxBits = 4097;
  29. private readonly Stream _fileStream;
  30. private readonly CancellationToken _currentCtsToken;
  31. private readonly bool _hasFrameBackups;
  32. private int _gctSize, _prevFrame = -1, _backupFrame = -1;
  33. private bool _gctUsed;
  34. private GifRect _gifDimensions;
  35. private readonly int _backBufferBytes;
  36. private GifColor[]? _bitmapBackBuffer;
  37. private short[]? _prefixBuf;
  38. private byte[]? _suffixBuf;
  39. private byte[]? _pixelStack;
  40. private byte[]? _indexBuf;
  41. private byte[]? _backupFrameIndexBuf;
  42. private volatile bool _hasNewFrame;
  43. public GifHeader? Header { get; private set; }
  44. public readonly List<GifFrame> Frames = new();
  45. public PixelSize Size => new(Header?.Dimensions.Width ?? 0, Header?.Dimensions.Height ?? 0);
  46. public GifDecoder(Stream fileStream, CancellationToken currentCtsToken)
  47. {
  48. _fileStream = fileStream;
  49. _currentCtsToken = currentCtsToken;
  50. ProcessHeaderData();
  51. ProcessFrameData();
  52. if (Header != null)
  53. Header.IterationCount = Header.Iterations switch
  54. {
  55. -1 => new GifRepeatBehavior { Count = 1 },
  56. 0 => new GifRepeatBehavior { LoopForever = true },
  57. > 0 => new GifRepeatBehavior { Count = Header.Iterations },
  58. _ => Header.IterationCount
  59. };
  60. var pixelCount = _gifDimensions.TotalPixels;
  61. _hasFrameBackups = Frames
  62. .Any(f => f.FrameDisposalMethod == FrameDisposal.Restore);
  63. _bitmapBackBuffer = new GifColor[pixelCount];
  64. _indexBuf = new byte[pixelCount];
  65. if (_hasFrameBackups)
  66. _backupFrameIndexBuf = new byte[pixelCount];
  67. _prefixBuf = new short[MaxStackSize];
  68. _suffixBuf = new byte[MaxStackSize];
  69. _pixelStack = new byte[MaxStackSize + 1];
  70. _backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor));
  71. }
  72. public void Dispose()
  73. {
  74. Frames.Clear();
  75. _bitmapBackBuffer = null;
  76. _prefixBuf = null;
  77. _suffixBuf = null;
  78. _pixelStack = null;
  79. _indexBuf = null;
  80. _backupFrameIndexBuf = null;
  81. }
  82. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  83. private int PixCoord(int x, int y) => x + y * _gifDimensions.Width;
  84. static readonly (int Start, int Step)[] Pass =
  85. {
  86. (0, 8),
  87. (4, 8),
  88. (2, 4),
  89. (1, 2)
  90. };
  91. private void ClearImage()
  92. {
  93. if (_bitmapBackBuffer != null)
  94. Array.Fill(_bitmapBackBuffer, TransparentColor);
  95. _prevFrame = -1;
  96. _backupFrame = -1;
  97. }
  98. public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false)
  99. {
  100. if (_currentCtsToken.IsCancellationRequested)
  101. return;
  102. if (fIndex < 0 | fIndex >= Frames.Count)
  103. return;
  104. if (_prevFrame == fIndex)
  105. return;
  106. if (fIndex == 0 || forceClear || fIndex < _prevFrame)
  107. ClearImage();
  108. DisposePreviousFrame();
  109. _prevFrame++;
  110. // render intermediate frame
  111. for (int idx = _prevFrame; idx < fIndex; ++idx)
  112. {
  113. var prevFrame = Frames[idx];
  114. if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore)
  115. continue;
  116. if (prevFrame.FrameDisposalMethod == FrameDisposal.Background)
  117. {
  118. ClearArea(prevFrame.Dimensions);
  119. continue;
  120. }
  121. RenderFrameAt(idx, writeableBitmap);
  122. }
  123. RenderFrameAt(fIndex, writeableBitmap);
  124. }
  125. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  126. private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap)
  127. {
  128. var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
  129. var curFrame = Frames[idx];
  130. DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB);
  131. if (_hasFrameBackups & curFrame.ShouldBackup
  132. && _indexBuf != null && _backupFrameIndexBuf != null)
  133. {
  134. Buffer.BlockCopy(_indexBuf, 0,
  135. _backupFrameIndexBuf, 0,
  136. curFrame.Dimensions.TotalPixels);
  137. _backupFrame = idx;
  138. }
  139. DrawFrame(curFrame, _indexBuf);
  140. _prevFrame = idx;
  141. _hasNewFrame = true;
  142. using var lockedBitmap = writeableBitmap.Lock();
  143. WriteBackBufToFb(lockedBitmap.Address);
  144. ArrayPool<byte>.Shared.Return(tmpB);
  145. }
  146. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  147. private void DrawFrame(GifFrame curFrame, Memory<byte> frameIndexSpan)
  148. {
  149. var activeColorTable =
  150. curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header?.GlobalColorTable;
  151. var cX = curFrame.Dimensions.X;
  152. var cY = curFrame.Dimensions.Y;
  153. var cH = curFrame.Dimensions.Height;
  154. var cW = curFrame.Dimensions.Width;
  155. var tC = curFrame.TransparentColorIndex;
  156. var hT = curFrame.HasTransparency;
  157. if (curFrame.IsInterlaced)
  158. {
  159. int curSrcRow = 0;
  160. for (var i = 0; i < 4; i++)
  161. {
  162. var curPass = Pass[i];
  163. var y = curPass.Start;
  164. while (y < cH)
  165. {
  166. DrawRow(curSrcRow++, y);
  167. y += curPass.Step;
  168. }
  169. }
  170. }
  171. else
  172. {
  173. for (var i = 0; i < cH; i++)
  174. DrawRow(i, i);
  175. }
  176. return;
  177. void DrawRow(int srcRow, int destRow)
  178. {
  179. // Get the starting point of the current row on frame's index stream.
  180. var indexOffset = srcRow * cW;
  181. // Get the target backbuffer offset from the frames coords.
  182. var targetOffset = PixCoord(cX, destRow + cY);
  183. if (_bitmapBackBuffer == null) return;
  184. var len = _bitmapBackBuffer.Length;
  185. for (var i = 0; i < cW; i++)
  186. {
  187. var indexColor = frameIndexSpan.Span[indexOffset + i];
  188. if (activeColorTable == null || targetOffset >= len ||
  189. indexColor > activeColorTable.Length) return;
  190. if (!(hT & indexColor == tC))
  191. _bitmapBackBuffer[targetOffset] = activeColorTable[indexColor];
  192. targetOffset++;
  193. }
  194. }
  195. }
  196. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  197. private void DisposePreviousFrame()
  198. {
  199. if (_prevFrame == -1)
  200. return;
  201. var prevFrame = Frames[_prevFrame];
  202. switch (prevFrame.FrameDisposalMethod)
  203. {
  204. case FrameDisposal.Background:
  205. ClearArea(prevFrame.Dimensions);
  206. break;
  207. case FrameDisposal.Restore:
  208. if (_hasFrameBackups && _backupFrame != -1)
  209. DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf);
  210. else
  211. ClearArea(prevFrame.Dimensions);
  212. break;
  213. }
  214. }
  215. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  216. private void ClearArea(GifRect area)
  217. {
  218. if(_bitmapBackBuffer is null) return;
  219. for (var y = 0; y < area.Height; y++)
  220. {
  221. var targetOffset = PixCoord(area.X, y + area.Y);
  222. for (var x = 0; x < area.Width; x++)
  223. _bitmapBackBuffer[targetOffset + x] = TransparentColor;
  224. }
  225. }
  226. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  227. private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span<byte> indexSpan, byte[] tempBuf)
  228. {
  229. if (_prefixBuf is null || _suffixBuf is null || _pixelStack is null) return;
  230. _fileStream.Position = curFrame.LzwStreamPosition;
  231. var totalPixels = curFrame.Dimensions.TotalPixels;
  232. // Initialize GIF data stream decoder.
  233. var dataSize = curFrame.LzwMinCodeSize;
  234. var clear = 1 << dataSize;
  235. var endOfInformation = clear + 1;
  236. var available = clear + 2;
  237. var oldCode = -1;
  238. var codeSize = dataSize + 1;
  239. var codeMask = (1 << codeSize) - 1;
  240. for (var code = 0; code < clear; code++)
  241. {
  242. _prefixBuf[code] = 0;
  243. _suffixBuf[code] = (byte)code;
  244. }
  245. // Decode GIF pixel stream.
  246. int bits, first, top, pixelIndex;
  247. var datum = bits = first = top = pixelIndex = 0;
  248. while (pixelIndex < totalPixels)
  249. {
  250. var blockSize = _fileStream.ReadBlock(tempBuf);
  251. if (blockSize == 0)
  252. break;
  253. var blockPos = 0;
  254. while (blockPos < blockSize)
  255. {
  256. datum += tempBuf[blockPos] << bits;
  257. blockPos++;
  258. bits += 8;
  259. while (bits >= codeSize)
  260. {
  261. // Get the next code.
  262. var code = datum & codeMask;
  263. datum >>= codeSize;
  264. bits -= codeSize;
  265. // Interpret the code
  266. if (code == clear)
  267. {
  268. // Reset decoder.
  269. codeSize = dataSize + 1;
  270. codeMask = (1 << codeSize) - 1;
  271. available = clear + 2;
  272. oldCode = -1;
  273. continue;
  274. }
  275. // Check for explicit end-of-stream
  276. if (code == endOfInformation)
  277. return;
  278. if (oldCode == -1)
  279. {
  280. indexSpan[pixelIndex++] = _suffixBuf[code];
  281. oldCode = code;
  282. first = code;
  283. continue;
  284. }
  285. var inCode = code;
  286. if (code >= available)
  287. {
  288. _pixelStack[top++] = (byte)first;
  289. code = oldCode;
  290. if (top == MaxBits)
  291. ThrowLswException();
  292. }
  293. while (code >= clear)
  294. {
  295. if (code >= MaxBits || code == _prefixBuf[code])
  296. ThrowLswException();
  297. _pixelStack[top++] = _suffixBuf[code];
  298. code = _prefixBuf[code];
  299. if (top == MaxBits)
  300. ThrowLswException();
  301. }
  302. first = _suffixBuf[code];
  303. _pixelStack[top++] = (byte)first;
  304. // Add new code to the dictionary
  305. if (available < MaxStackSize)
  306. {
  307. _prefixBuf[available] = (short)oldCode;
  308. _suffixBuf[available] = (byte)first;
  309. available++;
  310. if ((available & codeMask) == 0 && available < MaxStackSize)
  311. {
  312. codeSize++;
  313. codeMask += available;
  314. }
  315. }
  316. oldCode = inCode;
  317. // Drain the pixel stack.
  318. do
  319. {
  320. indexSpan[pixelIndex++] = _pixelStack[--top];
  321. } while (top > 0);
  322. }
  323. }
  324. }
  325. while (pixelIndex < totalPixels)
  326. indexSpan[pixelIndex++] = 0; // clear missing pixels
  327. }
  328. private static void ThrowLswException() => throw new LzwDecompressionException();
  329. /// <summary>
  330. /// Directly copies the <see cref="GifColor"/> struct array to a bitmap IntPtr.
  331. /// </summary>
  332. private void WriteBackBufToFb(IntPtr targetPointer)
  333. {
  334. if (_currentCtsToken.IsCancellationRequested)
  335. return;
  336. if (!(_hasNewFrame && _bitmapBackBuffer != null)) return;
  337. unsafe
  338. {
  339. fixed (void* src = &_bitmapBackBuffer[0])
  340. Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes,
  341. (uint)_backBufferBytes);
  342. _hasNewFrame = false;
  343. }
  344. }
  345. /// <summary>
  346. /// Processes GIF Header.
  347. /// </summary>
  348. private void ProcessHeaderData()
  349. {
  350. var str = _fileStream;
  351. var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
  352. var tempBuf = tmpB.AsSpan();
  353. var _ = str.Read(tmpB, 0, 6);
  354. if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span))
  355. throw new InvalidGifStreamException("Not a GIF stream.");
  356. if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) |
  357. tempBuf[..6].SequenceEqual(G89AMagic.Span)))
  358. throw new InvalidGifStreamException("Unsupported GIF Version: " +
  359. Encoding.ASCII.GetString(tempBuf[..6].ToArray()));
  360. ProcessScreenDescriptor(tmpB);
  361. Header = new GifHeader
  362. {
  363. Dimensions = _gifDimensions,
  364. GlobalColorTable =
  365. _gctUsed ? ProcessColorTable(ref str, tmpB, _gctSize) : Array.Empty<GifColor>(),
  366. HeaderSize = _fileStream.Position
  367. };
  368. ArrayPool<byte>.Shared.Return(tmpB);
  369. }
  370. /// <summary>
  371. /// Parses colors from file stream to target color table.
  372. /// </summary>
  373. private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors)
  374. {
  375. var nBytes = 3 * nColors;
  376. var target = new GifColor[nColors];
  377. var n = stream.Read(rawBufSpan, 0, nBytes);
  378. if (n < nBytes)
  379. throw new InvalidOperationException("Wrong color table bytes.");
  380. int i = 0, j = 0;
  381. while (i < nColors)
  382. {
  383. var r = rawBufSpan[j++];
  384. var g = rawBufSpan[j++];
  385. var b = rawBufSpan[j++];
  386. target[i++] = new GifColor(r, g, b);
  387. }
  388. return target;
  389. }
  390. /// <summary>
  391. /// Parses screen and other GIF descriptors.
  392. /// </summary>
  393. private void ProcessScreenDescriptor(byte[] tempBuf)
  394. {
  395. var width = _fileStream.ReadUShortS(tempBuf);
  396. var height = _fileStream.ReadUShortS(tempBuf);
  397. var packed = _fileStream.ReadByteS(tempBuf);
  398. _gctUsed = (packed & 0x80) != 0;
  399. _gctSize = 2 << (packed & 7);
  400. _ = _fileStream.ReadByteS(tempBuf);
  401. _gifDimensions = new GifRect(0, 0, width, height);
  402. _fileStream.Skip(1);
  403. }
  404. /// <summary>
  405. /// Parses all frame data.
  406. /// </summary>
  407. private void ProcessFrameData()
  408. {
  409. _fileStream.Position = Header?.HeaderSize ?? -1;
  410. var tempBuf = ArrayPool<byte>.Shared.Rent(MaxTempBuf);
  411. var terminate = false;
  412. var curFrame = 0;
  413. Frames.Add(new GifFrame());
  414. do
  415. {
  416. var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf);
  417. switch (blockType)
  418. {
  419. case BlockTypes.Empty:
  420. break;
  421. case BlockTypes.Extension:
  422. ProcessExtensions(ref curFrame, tempBuf);
  423. break;
  424. case BlockTypes.ImageDescriptor:
  425. ProcessImageDescriptor(ref curFrame, tempBuf);
  426. _fileStream.SkipBlocks(tempBuf);
  427. break;
  428. case BlockTypes.Trailer:
  429. Frames.RemoveAt(Frames.Count - 1);
  430. terminate = true;
  431. break;
  432. default:
  433. _fileStream.SkipBlocks(tempBuf);
  434. break;
  435. }
  436. // Break the loop when the stream is not valid anymore.
  437. if (_fileStream.Position >= _fileStream.Length & terminate == false)
  438. throw new InvalidProgramException("Reach the end of the filestream without trailer block.");
  439. } while (!terminate);
  440. ArrayPool<byte>.Shared.Return(tempBuf);
  441. }
  442. /// <summary>
  443. /// Parses GIF Image Descriptor Block.
  444. /// </summary>
  445. private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf)
  446. {
  447. var str = _fileStream;
  448. var currentFrame = Frames[curFrame];
  449. // Parse frame dimensions.
  450. var frameX = str.ReadUShortS(tempBuf);
  451. var frameY = str.ReadUShortS(tempBuf);
  452. var frameW = str.ReadUShortS(tempBuf);
  453. var frameH = str.ReadUShortS(tempBuf);
  454. frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX);
  455. frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY);
  456. currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH);
  457. // Unpack interlace and lct info.
  458. var packed = str.ReadByteS(tempBuf);
  459. currentFrame.IsInterlaced = (packed & 0x40) != 0;
  460. currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0;
  461. currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1);
  462. if (currentFrame.IsLocalColorTableUsed)
  463. currentFrame.LocalColorTable =
  464. ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize);
  465. currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf);
  466. currentFrame.LzwStreamPosition = str.Position;
  467. curFrame += 1;
  468. Frames.Add(new GifFrame());
  469. }
  470. /// <summary>
  471. /// Parses GIF Extension Blocks.
  472. /// </summary>
  473. private void ProcessExtensions(ref int curFrame, byte[] tempBuf)
  474. {
  475. var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf);
  476. switch (extType)
  477. {
  478. case ExtensionType.GraphicsControl:
  479. _fileStream.ReadBlock(tempBuf);
  480. var currentFrame = Frames[curFrame];
  481. var packed = tempBuf[0];
  482. currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2);
  483. if (currentFrame.FrameDisposalMethod != FrameDisposal.Restore
  484. && currentFrame.FrameDisposalMethod != FrameDisposal.Background)
  485. currentFrame.ShouldBackup = true;
  486. currentFrame.HasTransparency = (packed & 1) != 0;
  487. currentFrame.FrameDelay =
  488. TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10);
  489. if (currentFrame.FrameDelay <= FrameDelayThreshold)
  490. currentFrame.FrameDelay = FrameDelayDefault;
  491. currentFrame.TransparentColorIndex = tempBuf[3];
  492. break;
  493. case ExtensionType.Application:
  494. var blockLen = _fileStream.ReadBlock(tempBuf);
  495. var _ = tempBuf.AsSpan(0, blockLen);
  496. var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length);
  497. if (blockHeader.SequenceEqual(NetscapeMagic.Span))
  498. {
  499. var count = 1;
  500. while (count > 0)
  501. count = _fileStream.ReadBlock(tempBuf);
  502. var iterationCount = SpanToShort(tempBuf.AsSpan(1));
  503. if (Header != null)
  504. Header.Iterations = iterationCount;
  505. }
  506. else
  507. _fileStream.SkipBlocks(tempBuf);
  508. break;
  509. default:
  510. _fileStream.SkipBlocks(tempBuf);
  511. break;
  512. }
  513. }
  514. }