diff --git a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXNFT.jslib b/Runtime/Plugins/HyperlinkXNFT/HyperlinkXNFT.jslib
deleted file mode 100644
index 3d9f20d4..00000000
--- a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXNFT.jslib
+++ /dev/null
@@ -1,7 +0,0 @@
-mergeInto(LibraryManager.library, {
- HyperlinkXNFT : function(linkUrl)
- {
- url = UTF8ToString(linkUrl);
- window.xnft.openWindow(url);
- }
-});
\ No newline at end of file
diff --git a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXNFT.jslib.meta b/Runtime/Plugins/HyperlinkXNFT/HyperlinkXNFT.jslib.meta
deleted file mode 100644
index 13192875..00000000
--- a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXNFT.jslib.meta
+++ /dev/null
@@ -1,32 +0,0 @@
-fileFormatVersion: 2
-guid: c8c74178128e4d14ca71cd1e85c2140d
-PluginImporter:
- externalObjects: {}
- serializedVersion: 2
- iconMap: {}
- executionOrder: {}
- defineConstraints: []
- isPreloaded: 0
- isOverridable: 0
- isExplicitlyReferenced: 0
- validateReferences: 1
- platformData:
- - first:
- Any:
- second:
- enabled: 0
- settings: {}
- - first:
- Editor: Editor
- second:
- enabled: 0
- settings:
- DefaultValueInitialized: true
- - first:
- WebGL: WebGL
- second:
- enabled: 1
- settings: {}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXnft.cs b/Runtime/Plugins/HyperlinkXNFT/HyperlinkXnft.cs
deleted file mode 100644
index da98c08f..00000000
--- a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXnft.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Runtime.InteropServices;
-using UnityEngine;
-
-// ReSharper disable once CheckNamespace
-
-public class HyperlinkXnft : MonoBehaviour
-{
- #if UNITY_WEBGL
- [DllImport("__Internal")]
- private static extern void HyperlinkXNFT(string linkUrl);
- #else
- private static void HyperlinkXNFT(string linkUrl){}
- #endif
-
-
- public void OpenLink(string link)
- {
- //xnft link has to start with https:// and not have "www" after it. Very important!
- #if UNITY_EDITOR
- Application.OpenURL(link);
- #else
- Application.OpenURL(link);
- HyperlinkXNFT(link);
- #endif
- }
-
-
-}
diff --git a/Runtime/Plugins/HyperlinkXnft.meta b/Runtime/Plugins/Unity-GifDecoder.meta
similarity index 77%
rename from Runtime/Plugins/HyperlinkXnft.meta
rename to Runtime/Plugins/Unity-GifDecoder.meta
index c80e333f..eb1063d4 100644
--- a/Runtime/Plugins/HyperlinkXnft.meta
+++ b/Runtime/Plugins/Unity-GifDecoder.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: f44cd08d984fb4bca925d6ad03351c20
+guid: 155e4e4b8926e4ffbbe5a11e44156b11
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode.meta b/Runtime/Plugins/Unity-GifDecoder/Decode.meta
new file mode 100644
index 00000000..d30ba7db
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e01ec37b89542df46a5772158c16c7be
+timeCreated: 1584261931
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode/GifBitBlockReader.cs b/Runtime/Plugins/Unity-GifDecoder/Decode/GifBitBlockReader.cs
new file mode 100644
index 00000000..28a3738b
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode/GifBitBlockReader.cs
@@ -0,0 +1,114 @@
+using System.IO;
+using ThreeDISevenZeroR.UnityGifDecoder.Utils;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder
+{
+ ///
+ /// Reader for GIF Blocks
+ ///
+ public class GifBitBlockReader
+ {
+ private Stream stream;
+ private int currentByte;
+ private int currentBitPosition;
+ private int currentBufferPosition;
+ private int currentBufferSize;
+ private bool endReached;
+ private readonly byte[] buffer;
+
+ public GifBitBlockReader()
+ {
+ buffer = new byte[256];
+ }
+
+ public GifBitBlockReader(Stream stream) : this()
+ {
+ SetStream(stream);
+ }
+
+ ///
+ /// Set new stream
+ ///
+ public void SetStream(Stream stream)
+ {
+ this.stream = stream;
+ }
+
+ ///
+ /// Read first block and initialize reading
+ ///
+ public void StartNewReading()
+ {
+ currentByte = 0;
+ currentBitPosition = 8;
+ ReadNextBlock();
+ }
+
+ ///
+ /// Skips to the last block, if end is not reached
+ ///
+ public void FinishReading()
+ {
+ while (!endReached)
+ {
+ ReadNextBlock();
+ }
+ }
+
+ ///
+ /// Read bits from stream and construct value
+ ///
+ /// Bit count to read
+ /// Value from readed bits
+ public int ReadBits(int count)
+ {
+ var result = 0;
+ var bitsToRead = count;
+ var offset = 0;
+ var bitsAvailable = 8 - currentBitPosition;
+
+ while(bitsToRead > 0)
+ {
+ if (currentBitPosition >= 8)
+ {
+ currentBitPosition = 0;
+ bitsAvailable = 8;
+
+ if (endReached)
+ {
+ // Some gifs can read slightly past end of a stream
+ // (since there is a zero byte afterwards anyway, it is safe to return 0)
+ currentByte = 0;
+ }
+ else
+ {
+ currentByte = buffer[currentBufferPosition++];
+ if (currentBufferPosition == currentBufferSize)
+ ReadNextBlock();
+ }
+ }
+
+ var mask = (byte) (((1 << bitsToRead) - 1) << currentBitPosition);
+ var readCount = bitsAvailable < bitsToRead ? bitsAvailable : bitsToRead;
+
+ result += ((mask & currentByte) >> currentBitPosition) << offset;
+
+ currentBitPosition += readCount;
+ bitsToRead -= readCount;
+ offset += readCount;
+ }
+
+ return result;
+ }
+
+ private void ReadNextBlock()
+ {
+ currentBufferSize = stream.ReadByte8();
+ currentBufferPosition = 0;
+ endReached = currentBufferSize == 0;
+
+ if(!endReached)
+ stream.Read(buffer, 0, currentBufferSize);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode/GifBitBlockReader.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Decode/GifBitBlockReader.cs.meta
new file mode 100644
index 00000000..920104ae
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode/GifBitBlockReader.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: f9dc432c149e91f49a0437d31fe5750b
+timeCreated: 1584261218
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode/GifCanvas.cs b/Runtime/Plugins/Unity-GifDecoder/Decode/GifCanvas.cs
new file mode 100644
index 00000000..cf5ff8c7
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode/GifCanvas.cs
@@ -0,0 +1,251 @@
+using System;
+using ThreeDISevenZeroR.UnityGifDecoder.Model;
+using UnityEngine;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder
+{
+ ///
+ /// GIF Canvas buffer for drawing decoded frames
+ ///
+ public class GifCanvas
+ {
+ ///
+ /// Get color array from this Canvas
+ /// For performance reasons, actual canvas array is returned, so
+ ///
+ public Color32[] Colors => canvasColors;
+
+ ///
+ /// Since pixel rows for Texture2D start from bottom, original gif image will look upside down
+ ///
+ /// So gif decoder can flip image Without performance hit,
+ /// and you can provide resulting array to Texture2D without flipping it manually
+ ///
+ /// Default value is TRUE, if you want original color order
+ /// (which will look flipped on Texture2D) you can set this to false
+ ///
+ public bool FlipVertically { get; set; } = true;
+
+ ///
+ /// Color which will be used for background fill
+ /// Note, that background is always transparent, so only r, g, b components are used (alpha is 0)
+ ///
+ public Color32 BackgroundColor { get; set; }
+
+ private Color32[] canvasColors;
+ private Color32[] revertDisposalBuffer;
+ private int canvasWidth;
+ private int canvasHeight;
+ private bool canvasIsEmpty;
+
+ private Color32[] framePalette;
+ private GifDisposalMethod frameDisposalMethod;
+
+ private int frameCanvasPosition;
+ private int frameCanvasRowEndPosition;
+ private int frameTransparentColorIndex;
+ private int frameRowCurrent;
+ private int frameX;
+ private int frameY;
+ private int frameWidth;
+ private int frameHeight;
+ private int[] frameRowStart;
+ private int[] frameRowEnd;
+
+ public GifCanvas()
+ {
+ canvasIsEmpty = true;
+ }
+
+ public GifCanvas(int width, int height) : this()
+ {
+ SetSize(width, height);
+ }
+
+ ///
+ /// Resizes canvas and resets its initial state
+ ///
+ /// New canvas width
+ /// New canvas height
+ public void SetSize(int width, int height)
+ {
+ if (width != canvasWidth || height != canvasHeight)
+ {
+ var size = width * height;
+ canvasColors = new Color32[size];
+ frameRowStart = new int[height];
+ frameRowEnd = new int[height];
+ revertDisposalBuffer = null;
+
+ canvasWidth = width;
+ canvasHeight = height;
+ }
+
+ Reset();
+ }
+
+ ///
+ /// Clears canvas colors and resets it to initial state
+ ///
+ public void Reset()
+ {
+ frameDisposalMethod = GifDisposalMethod.Keep;
+ frameX = 0;
+ frameY = 0;
+ frameWidth = canvasWidth;
+ frameHeight = canvasHeight;
+
+ if (!canvasIsEmpty)
+ {
+ FillWithColor(0, 0, canvasWidth, canvasHeight, new Color32(BackgroundColor.r, BackgroundColor.g, BackgroundColor.b, 0));
+ canvasIsEmpty = true;
+ }
+ }
+
+ ///
+ /// Method initiates new drawing, sequential calls to OutputPixel will draw everything on canvas
+ ///
+ /// Left offset of frame
+ /// Top offset of frame
+ /// Frame width
+ /// Frame height
+ /// Color palette for this frame
+ /// Index of transparent color, color from this index will be treated as transparent
+ /// Apply deinterlacing during drawing
+ /// Specifies, how to handle this frame when next frame is drawn
+ public void BeginNewFrame(int x, int y, int width, int height, Color32[] palette,
+ int transparentColorIndex, bool isInterlaced, GifDisposalMethod disposalMethod)
+ {
+ switch (frameDisposalMethod)
+ {
+ case GifDisposalMethod.ClearToBackgroundColor:
+ FillWithColor(frameX, frameY, frameWidth, frameHeight,
+ new Color32(BackgroundColor.r, BackgroundColor.g, BackgroundColor.b, 0));
+
+ break;
+
+ case GifDisposalMethod.Revert:
+ if(disposalMethod != GifDisposalMethod.Keep)
+ Array.Copy(revertDisposalBuffer, 0, canvasColors, 0, revertDisposalBuffer.Length);
+ break;
+ }
+
+ switch (disposalMethod)
+ {
+ case GifDisposalMethod.Revert:
+ if (revertDisposalBuffer == null)
+ revertDisposalBuffer = new Color32[canvasColors.Length];
+
+ Array.Copy(canvasColors, 0,
+ revertDisposalBuffer, 0, revertDisposalBuffer.Length);
+ break;
+ }
+
+ framePalette = palette;
+ frameDisposalMethod = disposalMethod;
+ canvasIsEmpty = false;
+ frameWidth = width;
+ frameHeight = height;
+ frameX = x;
+ frameY = y;
+
+ // Start before canvas, so next pixel output will load correct region
+ frameCanvasPosition = 0;
+ frameRowCurrent = -1;
+ frameCanvasRowEndPosition = -1;
+ frameTransparentColorIndex = transparentColorIndex;
+
+ RouteFrameDrawing(x, y, width, height, isInterlaced);
+ }
+
+ ///
+ /// Place pixel on canvas
+ ///
+ /// Pixel will be placed inside region specified by "BeginNewFrame",
+ /// sequential calls to "OutputPixel" will fill region eventually
+ ///
+ /// Index of color from palette to place on canvas
+ public void OutputPixel(int color)
+ {
+ if (frameCanvasPosition >= frameCanvasRowEndPosition)
+ {
+ frameRowCurrent++;
+ frameCanvasPosition = frameRowStart[frameRowCurrent];
+ frameCanvasRowEndPosition = frameRowEnd[frameRowCurrent];
+ }
+
+ if (color != frameTransparentColorIndex)
+ canvasColors[frameCanvasPosition] = framePalette[color];
+
+ frameCanvasPosition++;
+ }
+
+ ///
+ /// Fill specified region with single color
+ ///
+ public void FillWithColor(int x, int y, int width, int height, Color32 color)
+ {
+ if (width == canvasWidth && height == canvasHeight)
+ {
+ for (var i = canvasColors.Length - 1; i >= 0; i--)
+ canvasColors[i] = color;
+ }
+ else
+ {
+ int yStart;
+ int yEnd;
+
+ if (FlipVertically)
+ {
+ yEnd = (canvasHeight - y) * canvasWidth + x;
+ yStart = yEnd - canvasWidth * height;
+ }
+ else
+ {
+ yStart = y * canvasWidth + x;
+ yEnd = yStart + height * canvasWidth;
+ }
+
+ for (var ySrc = yStart; ySrc < yEnd; ySrc += canvasWidth)
+ {
+ var rowEnd = ySrc + width;
+ for (var i = ySrc; i < rowEnd; i++)
+ canvasColors[i] = color;
+ }
+ }
+ }
+
+ ///
+ /// Plan most optimal image drawing route
+ /// So colors can be written to final locations right from the start,
+ /// without intermediate buffers or sorting
+ ///
+ private void RouteFrameDrawing(int x, int y, int width, int height, bool deinterlace)
+ {
+ var currentRow = 0;
+
+ void ScheduleRowIndex(int row)
+ {
+ var startPosition = FlipVertically
+ ? (canvasHeight - 1 - (y + row)) * canvasWidth + x
+ : (y + row) * canvasWidth + x;
+
+ frameRowStart[currentRow] = startPosition;
+ frameRowEnd[currentRow] = startPosition + width;
+ currentRow++;
+ }
+
+ if (deinterlace)
+ {
+ for (var i = 0; i < height; i += 8) ScheduleRowIndex(i); // every 8, start with 0
+ for (var i = 4; i < height; i += 8) ScheduleRowIndex(i); // every 8, start with 4
+ for (var i = 2; i < height; i += 4) ScheduleRowIndex(i); // every 4, start with 2
+ for (var i = 1; i < height; i += 2) ScheduleRowIndex(i); // every 2, start with 1
+ }
+ else
+ {
+ for (var i = 0; i < height; i++) ScheduleRowIndex(i); // every row in order
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode/GifCanvas.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Decode/GifCanvas.cs.meta
new file mode 100644
index 00000000..fe97371c
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode/GifCanvas.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 470f5c6ddbd05684582a60c4c3e23be5
+timeCreated: 1584270904
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode/GifLzwDictionary.cs b/Runtime/Plugins/Unity-GifDecoder/Decode/GifLzwDictionary.cs
new file mode 100644
index 00000000..7911790c
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode/GifLzwDictionary.cs
@@ -0,0 +1,178 @@
+using System;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder.Decode
+{
+ ///
+ /// LZW Dictionary used to decode bit stream
+ ///
+ public class GifLzwDictionary
+ {
+ private readonly int[] dictionaryEntryOffsets;
+ private readonly int[] dictionaryEntrySizes;
+ private byte[] dictionaryHeap;
+ private int dictionarySize;
+ private int dictionaryHeapPosition;
+
+ private int initialDictionarySize;
+ private int initialLzwCodeSize;
+ private int initialDictionaryHeapPosition;
+ private int nextLzwCodeGrowth;
+ private int currentMinLzwCodeSize;
+
+ private int codeSize;
+ private int clearCodeId;
+ private int stopCodeId;
+ private int lastCodeId;
+
+ private bool isFull;
+
+ ///
+ /// Creates new instance and allocates dictionary resources
+ ///
+ public GifLzwDictionary()
+ {
+ dictionaryEntryOffsets = new int[4096];
+ dictionaryEntrySizes = new int[4096];
+ dictionaryHeap = new byte[32768];
+ }
+
+ ///
+ /// Initializes dictionary with minimum code size
+ ///
+ /// new minimum lzw code size
+ public void InitWithWordSize(int minLzwCodeSize)
+ {
+ if (currentMinLzwCodeSize != minLzwCodeSize)
+ {
+ currentMinLzwCodeSize = minLzwCodeSize;
+ dictionaryHeapPosition = 0;
+ dictionarySize = 0;
+
+ var colorCount = 1 << minLzwCodeSize;
+
+ for (var i = 0; i < colorCount; i++)
+ {
+ dictionaryEntryOffsets[i] = dictionaryHeapPosition;
+ dictionaryEntrySizes[i] = 1;
+ dictionaryHeap[dictionaryHeapPosition++] = (byte) i;
+ }
+
+ initialDictionarySize = colorCount + 2;
+ initialLzwCodeSize = minLzwCodeSize + 1;
+ initialDictionaryHeapPosition = dictionaryHeapPosition;
+
+ clearCodeId = colorCount;
+ stopCodeId = colorCount + 1;
+ }
+
+ Clear();
+ }
+
+ ///
+ /// Clear dictionary contents
+ ///
+ public void Clear()
+ {
+ codeSize = initialLzwCodeSize;
+ dictionarySize = initialDictionarySize;
+ dictionaryHeapPosition = initialDictionaryHeapPosition;
+ nextLzwCodeGrowth = 1 << codeSize;
+ isFull = false;
+ lastCodeId = -1;
+ }
+
+ ///
+ /// Decode block reader to canvas
+ ///
+ public void DecodeStream(GifBitBlockReader reader, GifCanvas c)
+ {
+ while (true)
+ {
+ var entry = reader.ReadBits(codeSize);
+
+ if (entry == clearCodeId)
+ {
+ Clear();
+ continue;
+ }
+
+ if (entry == stopCodeId)
+ {
+ return;
+ }
+
+ // Decode
+ if (entry < dictionarySize)
+ {
+ if (lastCodeId >= 0)
+ CreateNewCode(lastCodeId, entry);
+
+ lastCodeId = entry;
+ }
+ else
+ {
+ lastCodeId = CreateNewCode(lastCodeId, lastCodeId);
+ }
+
+ // Output
+ var position = dictionaryEntryOffsets[lastCodeId];
+ var size = dictionaryEntrySizes[lastCodeId];
+ var heapEnd = position + size;
+
+ for (var i = position; i < heapEnd; i++)
+ c.OutputPixel(dictionaryHeap[i]);
+ }
+ }
+
+ ///
+ /// Create new dictionary entry from base entry
+ ///
+ public int CreateNewCode(int baseEntry, int deriveEntry)
+ {
+ if (isFull)
+ return -1;
+
+ var entryHeapPosition = dictionaryEntryOffsets[baseEntry];
+ var entrySize = dictionaryEntrySizes[baseEntry];
+ var newEntryOffset = dictionaryHeapPosition;
+ var newEntrySize = entrySize + 1;
+ var newHeapCapacity = newEntryOffset + newEntrySize;
+
+ if (dictionaryHeap.Length < newHeapCapacity)
+ Array.Resize(ref dictionaryHeap, Math.Max(dictionaryHeap.Length * 2, newHeapCapacity));
+
+ if (entrySize < 12)
+ {
+ // It is faster to just copy array manually for small values
+ var endValue = entryHeapPosition + entrySize;
+ for (var i = entryHeapPosition; i < endValue; i++)
+ dictionaryHeap[dictionaryHeapPosition++] = dictionaryHeap[i];
+ }
+ else
+ {
+ Buffer.BlockCopy(dictionaryHeap, entryHeapPosition,
+ dictionaryHeap, dictionaryHeapPosition, entrySize);
+ dictionaryHeapPosition += entrySize;
+ }
+
+ dictionaryHeap[dictionaryHeapPosition++] = deriveEntry < initialDictionarySize
+ ? (byte) deriveEntry : dictionaryHeap[dictionaryEntryOffsets[deriveEntry]];
+
+ var insertPosition = dictionarySize++;
+ dictionaryEntryOffsets[insertPosition] = newEntryOffset;
+ dictionaryEntrySizes[insertPosition] = newEntrySize;
+
+ if (dictionarySize >= nextLzwCodeGrowth)
+ {
+ codeSize++;
+ nextLzwCodeGrowth = codeSize == 12 ? int.MaxValue : 1 << codeSize;
+ }
+
+ // Dictionary is capped at 4096 elements
+ if (dictionarySize >= 4096)
+ isFull = true;
+
+ return insertPosition;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Decode/GifLzwDictionary.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Decode/GifLzwDictionary.cs.meta
new file mode 100644
index 00000000..62a863e2
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Decode/GifLzwDictionary.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 10b7c7e7b7e1b344e8374d4f68299942
+timeCreated: 1584261938
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/GifStream.cs b/Runtime/Plugins/Unity-GifDecoder/GifStream.cs
new file mode 100644
index 00000000..0c3b6290
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/GifStream.cs
@@ -0,0 +1,635 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using ThreeDISevenZeroR.UnityGifDecoder.Decode;
+using ThreeDISevenZeroR.UnityGifDecoder.Model;
+using ThreeDISevenZeroR.UnityGifDecoder.Utils;
+using UnityEngine;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder
+{
+ ///
+ /// Main class for gif decoding
+ ///
+ /// Reads and decodes gif file sequentially in PullParser manner
+ /// This class can be called from any thread but one at a time, there is no thead safety mechanism
+ ///
+ /// Example usage:
+ ///
+ /// using (var gifStream = new GifStream(<yourFile>))
+ /// {
+ /// while (gifStream.HasMoreData)
+ /// {
+ /// switch (gifStream.CurrentToken)
+ /// {
+ /// case GifStream.Token.Image:
+ /// var image = gifStream.ReadImage();
+ /// // do something with image
+ /// break;
+ ///
+ /// case GifStream.Token.Comment:
+ /// var comment = gifStream.ReadComment();
+ /// // log this comment
+ /// break;
+ ///
+ /// default:
+ /// gifStream.SkipToken();
+ /// // this token has no use for you, skip it
+ /// break;
+ /// }
+ /// }
+ /// }
+ ///
+ ///
+ public class GifStream : IDisposable
+ {
+ ///
+ /// See:
+ ///
+ public bool FlipVertically
+ {
+ get => canvas.FlipVertically;
+ set => canvas.FlipVertically = value;
+ }
+
+ ///
+ /// Plain text block is not fully supported (since no one uses it), but if you want,
+ /// you can at least fill text drawing region with background color
+ ///
+ /// False by default, since web browsers skip text rendering completely
+ ///
+ public bool DrawPlainTextBackground { get; set; }
+
+ ///
+ /// Last encountered header data from stream
+ ///
+ public GifHeader Header => header;
+
+ ///
+ /// Is this stream hast more data or it is completed.
+ /// If this value is False, you can close this stream or call Reset() and read everything again
+ ///
+ /// Essentially it equals to CurrentToken != EndOfFile
+ ///
+ public bool HasMoreData => CurrentToken != Token.EndOfFile;
+
+ ///
+ /// Current stream token
+ /// You should call matching read method or skip it
+ ///
+ public Token CurrentToken { get; private set; }
+
+ ///
+ /// Underlying stream which is used for gif data loading
+ ///
+ public Stream BaseStream
+ {
+ get => currentStream;
+ set => SetStream(value);
+ }
+
+ private Stream currentStream;
+ private long headerStartPosition;
+ private long firstFrameStartPosition;
+
+ private GifHeader header;
+ private GifGraphicControl graphicControl;
+ private GifImageDescriptor imageDescriptor;
+
+ private GifCanvas canvas;
+ private GifLzwDictionary lzwDictionary;
+ private GifBitBlockReader blockReader;
+
+ private Color32[] globalColorTable;
+ private Color32[] localColorTable;
+ private readonly byte[] headerBuffer;
+ private readonly byte[] colorTableBuffer;
+ private readonly byte[] extensionApplicationBuffer;
+ private bool nextPaletteIsGlobal;
+
+ ///
+ /// Creates GifStream instance without Stream and preallocates resources for gif decoding
+ ///
+ public GifStream()
+ {
+ lzwDictionary = new GifLzwDictionary();
+ canvas = new GifCanvas();
+ blockReader = new GifBitBlockReader();
+
+ globalColorTable = new Color32[256];
+ localColorTable = new Color32[256];
+ headerBuffer = new byte[6];
+ extensionApplicationBuffer = new byte[11];
+ colorTableBuffer = new byte[768];
+ }
+
+ ///
+ /// Convenience constructor
+ /// Invokes original constructor and sets stream to read from
+ ///
+ /// Stream to read gif from
+ public GifStream(Stream stream) : this()
+ {
+ SetStream(stream);
+ }
+
+ ///
+ /// Convenience constructor
+ /// Invokes original constructor and sets MemoryStream with specified bytes
+ ///
+ /// bytes of gif file
+ public GifStream(byte[] gifBytes) : this(new MemoryStream(gifBytes)) { }
+
+ ///
+ /// Convenience constructor
+ /// Invokes original constructor and open stream from file path
+ /// Don't forget to call Dispose() to close file
+ ///
+ /// Path to gif file
+ public GifStream(string path) : this(File.OpenRead(path)) { }
+
+ ///
+ /// Sets new stream to read gif data from
+ ///
+ /// GifStream is reusable, you can change stream and read new gif from it.
+ /// That way you reuse allocations that you've made, and keep your memory usage to minimum
+ ///
+ /// Stream will be reset to its initial state
+ ///
+ /// new stream with gif data
+ /// Dispose previous stream
+ public void SetStream(Stream stream, bool disposePrevious = false)
+ {
+ if (disposePrevious)
+ currentStream?.Dispose();
+
+ header = new GifHeader();
+ imageDescriptor = new GifImageDescriptor();
+ graphicControl = new GifGraphicControl();
+
+ currentStream = stream;
+ CurrentToken = Token.Header;
+ blockReader.SetStream(stream);
+ }
+
+ ///
+ /// Disposes underlying Stream
+ ///
+ public void Dispose()
+ {
+ currentStream?.Dispose();
+ }
+
+ ///
+ /// Skips current token
+ ///
+ /// Despite the name, this method not always skips data, it will read and decode all image related data,
+ /// since next image rendering will break if this data is skipped.
+ /// But it skips comments, extensions or plain text blocks without memory allocations
+ ///
+ /// If this is unskippable token
+ public void SkipToken()
+ {
+ switch (CurrentToken)
+ {
+ case Token.Header: ReadHeader(); break;
+ case Token.Palette: ReadPalette(); break;
+ case Token.GraphicsControl: ReadGraphicsControl(); break;
+ case Token.ImageDescriptor: ReadImageDescriptor(); break;
+ case Token.Image: ReadImage(); break;
+ case Token.Comment: SkipComment(); break;
+ case Token.PlainText: SkipPlainText(); break;
+ case Token.NetscapeExtension: SkipNetscapeExtension(); break;
+ case Token.ApplicationExtension: SkipApplicationExtension(); break;
+ default: throw new InvalidOperationException($"Cannot skip token {CurrentToken}");
+ }
+ }
+
+ ///
+ /// Resets gif stream state, so you can read it from beginning again
+ /// This is useful when you need to playback gif from memory
+ ///
+ public void Reset(bool skipHeader = true, bool resetCanvas = true)
+ {
+ var targetPosition = skipHeader && firstFrameStartPosition != -1
+ ? firstFrameStartPosition
+ : headerStartPosition;
+
+ if (currentStream.Position != targetPosition)
+ currentStream.Position = targetPosition;
+
+ SetCurrentToken(Token.Header);
+
+ if (resetCanvas)
+ canvas.Reset();
+ }
+
+ ///
+ /// Read gif header
+ ///
+ /// Data of gif header
+ /// If file is not gif file
+ public GifHeader ReadHeader()
+ {
+ AssertToken(Token.Header);
+
+ // Header
+ headerStartPosition = currentStream.Position;
+ firstFrameStartPosition = -1;
+ currentStream.Read(headerBuffer, 0, headerBuffer.Length);
+
+ if(BitUtils.CheckString(headerBuffer, "GIF87a"))
+ header.version = GifVersion.Gif87a;
+ else if (BitUtils.CheckString(headerBuffer, "GIF89a"))
+ header.version = GifVersion.Gif89a;
+ else
+ throw new ArgumentException("Invalid or corrupted Gif file");
+
+ // Screen descriptor
+ header.width = currentStream.ReadInt16LittleEndian();
+ header.height = currentStream.ReadInt16LittleEndian();
+
+ var flags = currentStream.ReadByte8();
+ header.globalColorTableSize = BitUtils.GetColorTableSize(flags.GetBitsFromByte(0, 3));
+ header.sortColors = flags.GetBitFromByte(3);
+ header.colorResolution = flags.GetBitsFromByte(4, 3);
+ header.hasGlobalColorTable = flags.GetBitFromByte(7);
+
+ header.transparentColorIndex = currentStream.ReadByte8();
+ header.pixelAspectRatio = currentStream.ReadByte8();
+
+ canvas.SetSize(header.width, header.height);
+
+ if (header.hasGlobalColorTable)
+ {
+ SetCurrentToken(Token.Palette);
+ nextPaletteIsGlobal = true;
+ }
+ else
+ {
+ DetermineNextToken();
+ }
+
+ return header;
+ }
+
+ public GifPalette ReadPalette()
+ {
+ AssertToken(Token.Palette);
+
+ var size = nextPaletteIsGlobal ? header.globalColorTableSize : imageDescriptor.localColorTableSize;
+ var palette = nextPaletteIsGlobal ? globalColorTable : localColorTable;
+
+ currentStream.Read(colorTableBuffer, 0, size * 3);
+
+ var position = 0;
+ for (var i = 0; i < size; i++)
+ {
+ palette[i] = new Color32(
+ colorTableBuffer[position++],
+ colorTableBuffer[position++],
+ colorTableBuffer[position++],
+ 255);
+ }
+
+ if (nextPaletteIsGlobal)
+ {
+ firstFrameStartPosition = currentStream.Position;
+ DetermineNextToken();
+ }
+ else
+ {
+ SetCurrentToken(Token.Image);
+ }
+
+ return new GifPalette
+ {
+ palette = palette,
+ size = size,
+ isGlobal = nextPaletteIsGlobal
+ };
+ }
+
+ public GifGraphicControl ReadGraphicsControl()
+ {
+ AssertToken(Token.GraphicsControl);
+
+ currentStream.AssertByte(0x04);
+ var graphicsFlags = currentStream.ReadByte8();
+ var disposalMethodValue = graphicsFlags.GetBitsFromByte(2, 3);
+
+ graphicControl.hasTransparency = graphicsFlags.GetBitFromByte(0);
+ graphicControl.userInput = graphicsFlags.GetBitFromByte(1);
+ graphicControl.delayTime = currentStream.ReadInt16LittleEndian();
+ graphicControl.transparentColorIndex = currentStream.ReadByte8();
+
+ // Color index should be read anyway, so there is no point to not read original transparentColorIndex value
+ if (!graphicControl.hasTransparency)
+ graphicControl.transparentColorIndex = -1;
+
+ switch (disposalMethodValue)
+ {
+ case 0:
+ case 1: graphicControl.disposalMethod = GifDisposalMethod.Keep; break;
+ case 2: graphicControl.disposalMethod = GifDisposalMethod.ClearToBackgroundColor; break;
+ case 3: graphicControl.disposalMethod = GifDisposalMethod.Revert; break;
+ default: throw new ArgumentException($"Invalid disposal method type: {disposalMethodValue}");
+ }
+
+ currentStream.AssertByte(0x00);
+ DetermineNextToken();
+
+ return graphicControl;
+ }
+
+ public GifImageDescriptor ReadImageDescriptor()
+ {
+ AssertToken(Token.ImageDescriptor);
+ imageDescriptor.left = currentStream.ReadInt16LittleEndian();
+ imageDescriptor.top = currentStream.ReadInt16LittleEndian();
+ imageDescriptor.width = currentStream.ReadInt16LittleEndian();
+ imageDescriptor.height = currentStream.ReadInt16LittleEndian();
+
+ var flags = currentStream.ReadByte8();
+
+ imageDescriptor.localColorTableSize = BitUtils.GetColorTableSize(flags.GetBitsFromByte(0, 3));
+ imageDescriptor.isInterlaced = flags.GetBitFromByte(6);
+ imageDescriptor.hasLocalColorTable = flags.GetBitFromByte(7);
+
+ if (imageDescriptor.hasLocalColorTable)
+ {
+ nextPaletteIsGlobal = false;
+ SetCurrentToken(Token.Palette);
+ }
+ else
+ {
+ SetCurrentToken(Token.Image);
+ }
+
+ return imageDescriptor;
+ }
+
+ ///
+ /// Read and construct actual frame from previous encountered gif data
+ ///
+ ///
+ public GifImage ReadImage()
+ {
+ AssertToken(Token.Image);
+
+ var usedColorTable = imageDescriptor.hasLocalColorTable
+ ? localColorTable
+ : globalColorTable;
+
+ var lzwMinCodeSize = currentStream.ReadByte8();
+
+ if(lzwMinCodeSize == 0 || lzwMinCodeSize > 8)
+ throw new ArgumentException("Invalid lzw min code size");
+
+ DecodeLzwImageToCanvas(lzwMinCodeSize,
+ imageDescriptor.left, imageDescriptor.top,
+ imageDescriptor.width, imageDescriptor.height, usedColorTable,
+ graphicControl.transparentColorIndex,
+ imageDescriptor.isInterlaced, graphicControl.disposalMethod);
+ DetermineNextToken();
+
+ return new GifImage
+ {
+ colors = canvas.Colors,
+ userInput = graphicControl.userInput,
+ delay = graphicControl.delayTime
+ };
+ }
+
+ public string ReadComment()
+ {
+ AssertToken(Token.Comment);
+ var text = Encoding.ASCII.GetString(BitUtils.ReadGifBlocks(currentStream));
+ DetermineNextToken();
+ return text;
+ }
+
+ public void SkipComment() => SkipBlock(Token.Comment);
+
+ public GifPlainText ReadPlainText()
+ {
+ AssertToken(Token.PlainText);
+ currentStream.AssertByte(0x0c);
+
+ var result = new GifPlainText();
+ result.left = currentStream.ReadInt16LittleEndian();
+ result.top = currentStream.ReadInt16LittleEndian();
+ result.width = currentStream.ReadInt16LittleEndian();
+ result.height = currentStream.ReadInt16LittleEndian();
+ result.charWidth = currentStream.ReadByte8();
+ result.charHeight = currentStream.ReadByte8();
+ result.foregroundColor = globalColorTable[currentStream.ReadByte8()];
+ result.backgroundColor = globalColorTable[currentStream.ReadByte8()];
+ result.text = Encoding.ASCII.GetString(BitUtils.ReadGifBlocks(currentStream));
+ result.colors = canvas.Colors;
+
+ if (DrawPlainTextBackground)
+ FillPlainTextBackground(result);
+
+ DetermineNextToken();
+
+ return result;
+ }
+
+ public void SkipPlainText()
+ {
+ if (DrawPlainTextBackground)
+ ReadPlainText();
+ else
+ SkipBlock(Token.PlainText);
+ }
+
+ public GifNetscapeExtension ReadNetscapeExtension()
+ {
+ AssertToken(Token.NetscapeExtension);
+
+ var hasBufferSize = false;
+ var hasLoopCount = false;
+ var loopCount = 0;
+ var bufferSize = 0;
+
+ while (true)
+ {
+ var blockSize = currentStream.ReadByte8();
+
+ if (blockSize == 0)
+ break;
+
+ var blockId = currentStream.ReadByte8();
+
+ switch (blockId)
+ {
+ case 0x01:
+ hasLoopCount = true;
+ loopCount = currentStream.ReadInt16LittleEndian();
+ break;
+
+ case 0x02:
+ hasBufferSize = true;
+ bufferSize = currentStream.ReadInt32LittleEndian();
+ break;
+
+ default:
+ currentStream.Seek(blockSize - 1, SeekOrigin.Current);
+ break;
+ }
+ }
+
+ DetermineNextToken();
+
+ return new GifNetscapeExtension
+ {
+ hasLoopCount = hasLoopCount,
+ hasBufferSize = hasBufferSize,
+ loopCount = loopCount,
+ bufferSize = bufferSize
+ };
+ }
+
+ public void SkipNetscapeExtension() => SkipBlock(Token.NetscapeExtension);
+
+ public GifApplicationExtension ReadApplicationExtension()
+ {
+ AssertToken(Token.ApplicationExtension);
+
+ var blocks = new List();
+ var appName = Encoding.ASCII.GetString(extensionApplicationBuffer, 0, 8);
+ var appCode = Encoding.ASCII.GetString(extensionApplicationBuffer, 8, 3);
+
+ while (true)
+ {
+ var blockSize = currentStream.ReadByte8();
+
+ if (blockSize == 0)
+ break;
+
+ var array = new byte[blockSize];
+ currentStream.Read(array, 0, blockSize);
+ blocks.Add(array);
+ }
+
+ DetermineNextToken();
+
+ return new GifApplicationExtension
+ {
+ applicationIdentifier = appName,
+ applicationAuthCode = appCode,
+ applicationData = blocks.ToArray()
+ };
+ }
+
+ public void SkipApplicationExtension() => SkipBlock(Token.ApplicationExtension);
+
+ private void DecodeLzwImageToCanvas(int lzwMinCodeSize, int x, int y, int width, int height,
+ Color32[] colorTable, int transparentColorIndex, bool isInterlaced, GifDisposalMethod disposalMethod)
+ {
+ if (header.hasGlobalColorTable)
+ canvas.BackgroundColor = globalColorTable[header.transparentColorIndex];
+
+ canvas.BeginNewFrame(x, y, width, height, colorTable, transparentColorIndex, isInterlaced, disposalMethod);
+
+ lzwDictionary.InitWithWordSize(lzwMinCodeSize);
+ blockReader.StartNewReading();
+
+ lzwDictionary.DecodeStream(blockReader, canvas);
+ blockReader.FinishReading();
+ }
+
+ private Token DetermineNextToken()
+ {
+ while (true)
+ {
+ var blockType = currentStream.ReadByte8();
+ switch (blockType)
+ {
+ case ExtensionBlock:
+ var extensionType = currentStream.ReadByte8();
+ switch (extensionType)
+ {
+ case commentLabel: return SetCurrentToken(Token.Comment);
+ case PlainTextLabel: return SetCurrentToken(Token.PlainText);
+ case GraphicControlLabel: return SetCurrentToken(Token.GraphicsControl);
+ case applicationExtensionLabel:
+ {
+ currentStream.AssertByte(11);
+ currentStream.Read(extensionApplicationBuffer, 0, 11);
+
+ var token = BitUtils.CheckString(extensionApplicationBuffer, "NETSCAPE2.0")
+ ? Token.NetscapeExtension
+ : Token.ApplicationExtension;
+
+ return SetCurrentToken(token);
+ }
+
+ default: BitUtils.SkipGifBlocks(currentStream); break;
+ }
+
+ break;
+
+ case ImageDescriptorBlock: return SetCurrentToken(Token.ImageDescriptor);
+ case EndOfFile: return SetCurrentToken(Token.EndOfFile);
+ default: throw new ArgumentException($"Unknown block type {blockType}");
+ }
+ }
+ }
+
+ private Token SetCurrentToken(Token token)
+ {
+ CurrentToken = token;
+ return token;
+ }
+
+ private void FillPlainTextBackground(GifPlainText text)
+ {
+ canvas.BeginNewFrame(text.left, text.top, text.width, text.height, globalColorTable,
+ graphicControl.transparentColorIndex, imageDescriptor.isInterlaced, graphicControl.disposalMethod);
+
+ canvas.FillWithColor(text.left, text.top,
+ text.width, text.height, text.backgroundColor);
+ }
+
+ private void AssertToken(Token token)
+ {
+ if (CurrentToken != token)
+ throw new InvalidOperationException(
+ $"Cannot invoke this method while current token is \"{CurrentToken}\", " +
+ $"method should be called when token is {token}");
+ }
+
+ private void SkipBlock(Token token)
+ {
+ AssertToken(token);
+ BitUtils.SkipGifBlocks(currentStream);
+ DetermineNextToken();
+ }
+
+ public enum Token
+ {
+ Header,
+ Palette,
+ GraphicsControl,
+ ImageDescriptor,
+ Image,
+ Comment,
+ PlainText,
+ NetscapeExtension,
+ ApplicationExtension,
+ EndOfFile
+ }
+
+ private const int ExtensionBlock = 0x21;
+ private const int ImageDescriptorBlock = 0x2c;
+ private const int EndOfFile = 0x3b;
+
+ private const int PlainTextLabel = 0x01;
+ private const int GraphicControlLabel = 0xf9;
+ private const int commentLabel = 0xfe;
+ private const int applicationExtensionLabel = 0xff;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/GifStream.cs.meta b/Runtime/Plugins/Unity-GifDecoder/GifStream.cs.meta
new file mode 100644
index 00000000..58f74dd8
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/GifStream.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: be83d0f36f67fae43ac68bf2871b313d
+timeCreated: 1584375628
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model.meta b/Runtime/Plugins/Unity-GifDecoder/Model.meta
new file mode 100644
index 00000000..2d8e1688
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7abae67612954b22975208c6db81a513
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifApplicationExtension.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifApplicationExtension.cs
new file mode 100644
index 00000000..82aa044f
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifApplicationExtension.cs
@@ -0,0 +1,9 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public class GifApplicationExtension
+ {
+ public string applicationIdentifier;
+ public string applicationAuthCode;
+ public byte[][] applicationData;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifApplicationExtension.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifApplicationExtension.cs.meta
new file mode 100644
index 00000000..037312ae
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifApplicationExtension.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: d459845af26d4a6194cfe66a8334fde1
+timeCreated: 1584706466
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifDisposalMethod.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifDisposalMethod.cs
new file mode 100644
index 00000000..d7cf6521
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifDisposalMethod.cs
@@ -0,0 +1,20 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public enum GifDisposalMethod
+ {
+ ///
+ /// Keep previous frame and draw new frame on top of it
+ ///
+ Keep,
+
+ ///
+ /// Clear previous region
+ ///
+ ClearToBackgroundColor,
+
+ ///
+ /// Revert previous drawing operation, so canvas will contain previous frame
+ ///
+ Revert
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifDisposalMethod.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifDisposalMethod.cs.meta
new file mode 100644
index 00000000..a91cca73
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifDisposalMethod.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 40ef60b5839a41e3a2631ecae73e97ee
+timeCreated: 1584700393
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifGraphicControl.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifGraphicControl.cs
new file mode 100644
index 00000000..4a043425
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifGraphicControl.cs
@@ -0,0 +1,12 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public struct GifGraphicControl
+ {
+ public bool userInput;
+ public GifDisposalMethod disposalMethod;
+ public int delayTime;
+
+ public bool hasTransparency;
+ public int transparentColorIndex;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifGraphicControl.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifGraphicControl.cs.meta
new file mode 100644
index 00000000..b5fb478d
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifGraphicControl.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9381314b4c9e4554b13c7b33f1169fdc
+timeCreated: 1584700286
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifHeader.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifHeader.cs
new file mode 100644
index 00000000..ccf0d904
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifHeader.cs
@@ -0,0 +1,15 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public struct GifHeader
+ {
+ public GifVersion version;
+ public int width;
+ public int height;
+ public bool hasGlobalColorTable;
+ public int globalColorTableSize;
+ public int transparentColorIndex;
+ public bool sortColors;
+ public int colorResolution;
+ public int pixelAspectRatio;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXnft.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifHeader.cs.meta
similarity index 83%
rename from Runtime/Plugins/HyperlinkXNFT/HyperlinkXnft.cs.meta
rename to Runtime/Plugins/Unity-GifDecoder/Model/GifHeader.cs.meta
index 2bfe4fe7..3daa6a9a 100644
--- a/Runtime/Plugins/HyperlinkXNFT/HyperlinkXnft.cs.meta
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifHeader.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 882d26f261952cd448e600e58bd3009a
+guid: 6e38ee8bf9e449cd987f6110a7cc21d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifImage.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifImage.cs
new file mode 100644
index 00000000..f3ced8b8
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifImage.cs
@@ -0,0 +1,17 @@
+using UnityEngine;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public class GifImage
+ {
+ public bool userInput;
+ public Color32[] colors;
+ public int delay;
+
+ public int DelayMs => delay * 10;
+ public float SafeDelayMs => delay > 1 ? DelayMs : 100;
+
+ public float DelaySeconds => delay / 100f;
+ public float SafeDelaySeconds => SafeDelayMs / 1000f;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifImage.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifImage.cs.meta
new file mode 100644
index 00000000..781b14fe
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifImage.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8bee34face794211a984a2c82adda054
+timeCreated: 1584701668
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifImageDescriptor.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifImageDescriptor.cs
new file mode 100644
index 00000000..60bb8e3a
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifImageDescriptor.cs
@@ -0,0 +1,14 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public struct GifImageDescriptor
+ {
+ public int left;
+ public int top;
+ public int width;
+ public int height;
+
+ public bool isInterlaced;
+ public bool hasLocalColorTable;
+ public int localColorTableSize;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifImageDescriptor.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifImageDescriptor.cs.meta
new file mode 100644
index 00000000..ce1c2bee
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifImageDescriptor.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ae60cc77a5e443e78446970bbdcf19b3
+timeCreated: 1584701101
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifNetscapeExtension.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifNetscapeExtension.cs
new file mode 100644
index 00000000..7b758536
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifNetscapeExtension.cs
@@ -0,0 +1,11 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public struct GifNetscapeExtension
+ {
+ public bool hasLoopCount;
+ public bool hasBufferSize;
+
+ public int loopCount;
+ public int bufferSize;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifNetscapeExtension.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifNetscapeExtension.cs.meta
new file mode 100644
index 00000000..17eae227
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifNetscapeExtension.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ac22e19510bb4391b4341243fadcb447
+timeCreated: 1584709461
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifPalette.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifPalette.cs
new file mode 100644
index 00000000..5a7f74e7
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifPalette.cs
@@ -0,0 +1,11 @@
+using UnityEngine;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public struct GifPalette
+ {
+ public Color32[] palette;
+ public int size;
+ public bool isGlobal;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifPalette.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifPalette.cs.meta
new file mode 100644
index 00000000..4fbd091e
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifPalette.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 7753b4c661b54120b39a01a0a5b10fd7
+timeCreated: 1584698879
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifPlainText.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifPlainText.cs
new file mode 100644
index 00000000..dd947ede
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifPlainText.cs
@@ -0,0 +1,18 @@
+using UnityEngine;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public struct GifPlainText
+ {
+ public int left;
+ public int top;
+ public int width;
+ public int height;
+ public int charWidth;
+ public int charHeight;
+ public Color32 backgroundColor;
+ public Color32 foregroundColor;
+ public string text;
+ public Color32[] colors;
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifPlainText.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifPlainText.cs.meta
new file mode 100644
index 00000000..8107aaef
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifPlainText.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 7a7e97901add4e5888d645686dacfd93
+timeCreated: 1584704079
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifVersion.cs b/Runtime/Plugins/Unity-GifDecoder/Model/GifVersion.cs
new file mode 100644
index 00000000..fd6f6029
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifVersion.cs
@@ -0,0 +1,15 @@
+namespace ThreeDISevenZeroR.UnityGifDecoder.Model
+{
+ public enum GifVersion
+ {
+ ///
+ /// Gif specification from year 1989
+ ///
+ Gif89a,
+
+ ///
+ /// Gif specification from year 1987
+ ///
+ Gif87a
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Model/GifVersion.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Model/GifVersion.cs.meta
new file mode 100644
index 00000000..8aadcec5
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Model/GifVersion.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 8686482240924cd58cec5e093527005a
+timeCreated: 1584698024
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/UnityGifDecoder.asmdef b/Runtime/Plugins/Unity-GifDecoder/UnityGifDecoder.asmdef
new file mode 100644
index 00000000..6742fd3b
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/UnityGifDecoder.asmdef
@@ -0,0 +1,3 @@
+{
+ "name": "UnityGifDecoder"
+}
diff --git a/Runtime/Plugins/Unity-GifDecoder/UnityGifDecoder.asmdef.meta b/Runtime/Plugins/Unity-GifDecoder/UnityGifDecoder.asmdef.meta
new file mode 100644
index 00000000..5166a2c3
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/UnityGifDecoder.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b89b4e34254b84b42a6028250be63fd2
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Runtime/Plugins/Unity-GifDecoder/Utils.meta b/Runtime/Plugins/Unity-GifDecoder/Utils.meta
new file mode 100644
index 00000000..00809252
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Utils.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 743cbd90365aa83439ee931120385cce
+timeCreated: 1584270084
\ No newline at end of file
diff --git a/Runtime/Plugins/Unity-GifDecoder/Utils/BitUtils.cs b/Runtime/Plugins/Unity-GifDecoder/Utils/BitUtils.cs
new file mode 100644
index 00000000..bccd9fd1
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Utils/BitUtils.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace ThreeDISevenZeroR.UnityGifDecoder.Utils
+{
+ public static class BitUtils
+ {
+ public static bool CheckString(byte[] array, string s)
+ {
+ for (var i = 0; i < array.Length; i++)
+ {
+ if (array[i] != s[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ public static int ReadInt16LittleEndian(this Stream reader)
+ {
+ var b1 = reader.ReadByte8();
+ var b2 = reader.ReadByte8();
+ return (b2 << 8) + b1;
+ }
+
+ public static int ReadInt32LittleEndian(this Stream reader)
+ {
+ var b1 = reader.ReadByte8();
+ var b2 = reader.ReadByte8();
+ var b3 = reader.ReadByte8();
+ var b4 = reader.ReadByte8();
+ return (b4 << 24) + (b3 << 16) + (b2 << 8) + b1;
+ }
+
+ public static byte ReadByte8(this Stream reader)
+ {
+ var b = reader.ReadByte();
+
+ if (b == -1)
+ throw new EndOfStreamException();
+
+ return (byte) b;
+ }
+
+ public static void AssertByte(this Stream reader, int expectedValue)
+ {
+ var readByte = reader.ReadByte8();
+ if(readByte != expectedValue)
+ throw new ArgumentException($"Invalid byte, expected {expectedValue}, got {readByte}");
+ }
+
+ public static int GetColorTableSize(int data)
+ {
+ return 1 << (data + 1);
+ }
+
+ public static int GetBitsFromByte(this byte b, int offset, int count)
+ {
+ var result = 0;
+
+ for (var i = 0; i < count; i++)
+ {
+ result += (GetBitFromByte(b, offset + i) ? 1 : 0) << i;
+ }
+
+ return result;
+ }
+
+ public static bool GetBitFromByte(this byte b, int offset)
+ {
+ return (b & (1 << offset)) != 0;
+ }
+
+ public static byte[] ReadGifBlocks(Stream reader)
+ {
+ var blocks = new List();
+
+ while (true)
+ {
+ var blockSize = reader.ReadByte8();
+
+ if(blockSize == 0)
+ break;
+
+ var bytes = new byte[blockSize];
+ reader.Read(bytes, 0, bytes.Length);
+ blocks.AddRange(bytes);
+ }
+
+ return blocks.ToArray();
+ }
+
+ public static void SkipGifBlocks(Stream reader)
+ {
+ while (true)
+ {
+ var blockSize = reader.ReadByte8();
+
+ if (blockSize == 0)
+ return;
+
+ reader.Seek(blockSize, SeekOrigin.Current);
+ }
+ }
+ }
+}
diff --git a/Runtime/Plugins/Unity-GifDecoder/Utils/BitUtils.cs.meta b/Runtime/Plugins/Unity-GifDecoder/Utils/BitUtils.cs.meta
new file mode 100644
index 00000000..4a426dd3
--- /dev/null
+++ b/Runtime/Plugins/Unity-GifDecoder/Utils/BitUtils.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 26afa21a58161564c8e071e3a92c718f
+timeCreated: 1584222291
\ No newline at end of file
diff --git a/Runtime/codebase/utility/FileDownloader.cs b/Runtime/codebase/utility/FileDownloader.cs
index a21997ab..e18c247f 100644
--- a/Runtime/codebase/utility/FileDownloader.cs
+++ b/Runtime/codebase/utility/FileDownloader.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
@@ -6,9 +7,13 @@
using Newtonsoft.Json;
using Solana.Unity.Metaplex.NFT.Library;
using Solana.Unity.Wallet;
+using ThreeDISevenZeroR.UnityGifDecoder;
+using ThreeDISevenZeroR.UnityGifDecoder.Model;
using UnityEngine;
using UnityEngine.Networking;
+// ReSharper disable once CheckNamespace
+
namespace Solana.Unity.SDK.Utility
{
@@ -69,7 +74,14 @@ public static async Task LoadFile(string path, string optionalName = "")
if (typeof(T) == typeof(Texture2D))
{
- return await LoadTexture(path);
+ if (path.ToLower().EndsWith(".gif") || path.ToLower().EndsWith("ext=gif"))
+ {
+ return await LoadGif(path);
+ }
+ else
+ {
+ return await LoadTexture(path);
+ }
}
else
{
@@ -101,6 +113,68 @@ private static async Task LoadTexture(string filePath, CancellationToken t
tex.LoadImage(data);
return (T)Convert.ChangeType(tex, typeof(T));
}
+
+ private static async Task LoadGif(string path, CancellationToken token = default)
+ {
+ using (UnityWebRequest uwr = UnityWebRequest.Get(path))
+ {
+ uwr.SendWebRequest();
+ while (!uwr.isDone && !token.IsCancellationRequested)
+ {
+ await Task.Yield();
+ }
+
+ if (uwr.result == UnityWebRequest.Result.ConnectionError)
+ {
+ Debug.Log(uwr.error);
+ return default;
+ }
+
+ Texture mainTexture = GetTextureFromGifByteStream(uwr.downloadHandler.data);
+
+ var changeType = (T)Convert.ChangeType(mainTexture, typeof(T));
+ return changeType;
+ }
+ }
+
+ private static Texture2D GetTextureFromGifByteStream(byte[] bytes)
+ {
+ var frameDelays = new List();
+
+ using (var gifStream = new GifStream(bytes))
+ {
+ while (gifStream.HasMoreData)
+ {
+ switch (gifStream.CurrentToken)
+ {
+ case GifStream.Token.Image:
+ GifImage image = gifStream.ReadImage();
+ var frame = new Texture2D(
+ gifStream.Header.width,
+ gifStream.Header.height,
+ TextureFormat.ARGB32, false);
+
+ frame.SetPixels32(image.colors);
+ frame.Apply();
+
+ frameDelays.Add(image.SafeDelaySeconds);
+
+ return frame;
+
+ case GifStream.Token.Comment:
+ var commentText = gifStream.ReadComment();
+ Debug.Log(commentText);
+ break;
+
+ default:
+ gifStream.SkipToken(); // Other tokens
+ break;
+ }
+ }
+ }
+
+ return null;
+ }
private static async Task LoadJsonWebRequest(string path)
{
@@ -123,7 +197,7 @@ private static async Task LoadJsonWebRequest(string path)
Debug.Log(json);
try
{
- var data = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
+ var data = JsonConvert.DeserializeObject(json);
return data;
}
catch
diff --git a/Runtime/com.solana.unity_sdk.asmdef b/Runtime/com.solana.unity_sdk.asmdef
index 01848201..b7cf1898 100644
--- a/Runtime/com.solana.unity_sdk.asmdef
+++ b/Runtime/com.solana.unity_sdk.asmdef
@@ -5,7 +5,8 @@
"GUID:11b8985f735c7ea47bf017c34f3fcc41",
"Unity.TextMeshPro",
"GUID:f51ebe6a0ceec4240a699833d6309b23",
- "GUID:1eaf7438b68a141a39da26d98b990fe7"
+ "GUID:1eaf7438b68a141a39da26d98b990fe7",
+ "GUID:b89b4e34254b84b42a6028250be63fd2"
],
"includePlatforms": [],
"excludePlatforms": [],