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": [],