/* UniGif Copyright (c) 2015 WestHillApps (Hironari Nishioka) This software is released under the MIT License. http://opensource.org/licenses/mit-license.php */ using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; public static partial class UniGif { /// /// Decode to textures from GIF data /// /// GIF data /// Callback method(param is GIF texture list) /// Textures filter mode /// Textures wrap mode /// IEnumerator private static IEnumerator DecodeTextureCoroutine(GifData gifData, Action> callback, FilterMode filterMode, TextureWrapMode wrapMode) { if (gifData.m_imageBlockList == null || gifData.m_imageBlockList.Count < 1) { yield break; } List gifTexList = new List(gifData.m_imageBlockList.Count); List disposalMethodList = new List(gifData.m_imageBlockList.Count); int imgIndex = 0; for (int i = 0; i < gifData.m_imageBlockList.Count; i++) { byte[] decodedData = GetDecodedData(gifData.m_imageBlockList[i]); GraphicControlExtension? graphicCtrlEx = GetGraphicCtrlExt(gifData, imgIndex); int transparentIndex = GetTransparentIndex(graphicCtrlEx); disposalMethodList.Add(GetDisposalMethod(graphicCtrlEx)); Color32 bgColor; List colorTable = GetColorTableAndSetBgColor(gifData, gifData.m_imageBlockList[i], transparentIndex, out bgColor); yield return 0; bool filledTexture; Texture2D tex = CreateTexture2D(gifData, gifTexList, imgIndex, disposalMethodList, bgColor, filterMode, wrapMode, out filledTexture); yield return 0; // Set pixel data int dataIndex = 0; // Reverse set pixels. because GIF data starts from the top left. for (int y = tex.height - 1; y >= 0; y--) { SetTexturePixelRow(tex, y, gifData.m_imageBlockList[i], decodedData, ref dataIndex, colorTable, bgColor, transparentIndex, filledTexture); } tex.Apply(); yield return 0; float delaySec = GetDelaySec(graphicCtrlEx); // Add to GIF texture list gifTexList.Add(new GifTexture(tex, delaySec)); imgIndex++; } if (callback != null) { callback(gifTexList); } yield break; } #region Call from DecodeTexture methods /// /// Get decoded image data from ImageBlock /// private static byte[] GetDecodedData(ImageBlock imgBlock) { // Combine LZW compressed data List lzwData = new List(); for (int i = 0; i < imgBlock.m_imageDataList.Count; i++) { for (int k = 0; k < imgBlock.m_imageDataList[i].m_imageData.Length; k++) { lzwData.Add(imgBlock.m_imageDataList[i].m_imageData[k]); } } // LZW decode int needDataSize = imgBlock.m_imageHeight * imgBlock.m_imageWidth; byte[] decodedData = DecodeGifLZW(lzwData, imgBlock.m_lzwMinimumCodeSize, needDataSize); // Sort interlace GIF if (imgBlock.m_interlaceFlag) { decodedData = SortInterlaceGifData(decodedData, imgBlock.m_imageWidth); } return decodedData; } /// /// Get color table and set background color (local or global) /// private static List GetColorTableAndSetBgColor(GifData gifData, ImageBlock imgBlock, int transparentIndex, out Color32 bgColor) { List colorTable = imgBlock.m_localColorTableFlag ? imgBlock.m_localColorTable : gifData.m_globalColorTableFlag ? gifData.m_globalColorTable : null; if (colorTable != null) { // Set background color from color table byte[] bgRgb = colorTable[gifData.m_bgColorIndex]; bgColor = new Color32(bgRgb[0], bgRgb[1], bgRgb[2], (byte)(transparentIndex == gifData.m_bgColorIndex ? 0 : 255)); } else { bgColor = Color.black; } return colorTable; } /// /// Get GraphicControlExtension from GifData /// private static GraphicControlExtension? GetGraphicCtrlExt(GifData gifData, int imgBlockIndex) { if (gifData.m_graphicCtrlExList != null && gifData.m_graphicCtrlExList.Count > imgBlockIndex) { return gifData.m_graphicCtrlExList[imgBlockIndex]; } return null; } /// /// Get transparent color index from GraphicControlExtension /// private static int GetTransparentIndex(GraphicControlExtension? graphicCtrlEx) { int transparentIndex = -1; if (graphicCtrlEx != null && graphicCtrlEx.Value.m_transparentColorFlag) { transparentIndex = graphicCtrlEx.Value.m_transparentColorIndex; } return transparentIndex; } /// /// Get delay seconds from GraphicControlExtension /// private static float GetDelaySec(GraphicControlExtension? graphicCtrlEx) { // Get delay sec from GraphicControlExtension float delaySec = graphicCtrlEx != null ? graphicCtrlEx.Value.m_delayTime / 100f : (1f / 60f); if (delaySec <= 0f) { delaySec = 0.1f; } return delaySec; } /// /// Get disposal method from GraphicControlExtension /// private static ushort GetDisposalMethod(GraphicControlExtension? graphicCtrlEx) { return graphicCtrlEx != null ? graphicCtrlEx.Value.m_disposalMethod : (ushort)2; } /// /// Create Texture2D object and initial settings /// private static Texture2D CreateTexture2D(GifData gifData, List gifTexList, int imgIndex, List disposalMethodList, Color32 bgColor, FilterMode filterMode, TextureWrapMode wrapMode, out bool filledTexture) { filledTexture = false; // Create texture Texture2D tex = new Texture2D(gifData.m_logicalScreenWidth, gifData.m_logicalScreenHeight, TextureFormat.ARGB32, false); tex.filterMode = filterMode; tex.wrapMode = wrapMode; // Check dispose ushort disposalMethod = imgIndex > 0 ? disposalMethodList[imgIndex - 1] : (ushort)2; int useBeforeIndex = -1; if (disposalMethod == 0) { // 0 (No disposal specified) } else if (disposalMethod == 1) { // 1 (Do not dispose) useBeforeIndex = imgIndex - 1; } else if (disposalMethod == 2) { // 2 (Restore to background color) filledTexture = true; Color32[] pix = new Color32[tex.width * tex.height]; for (int i = 0; i < pix.Length; i++) { pix[i] = bgColor; } tex.SetPixels32(pix); tex.Apply(); } else if (disposalMethod == 3) { // 3 (Restore to previous) for (int i = imgIndex - 1; i >= 0; i--) { if (disposalMethodList[i] == 0 || disposalMethodList[i] == 1) { useBeforeIndex = i; break; } } } if (useBeforeIndex >= 0) { filledTexture = true; Color32[] pix = gifTexList[useBeforeIndex].m_texture2d.GetPixels32(); tex.SetPixels32(pix); tex.Apply(); } return tex; } /// /// Set texture pixel row /// private static void SetTexturePixelRow(Texture2D tex, int y, ImageBlock imgBlock, byte[] decodedData, ref int dataIndex, List colorTable, Color32 bgColor, int transparentIndex, bool filledTexture) { // Row no (0~) int row = tex.height - 1 - y; for (int x = 0; x < tex.width; x++) { // Line no (0~) int line = x; // Out of image blocks if (row < imgBlock.m_imageTopPosition || row >= imgBlock.m_imageTopPosition + imgBlock.m_imageHeight || line < imgBlock.m_imageLeftPosition || line >= imgBlock.m_imageLeftPosition + imgBlock.m_imageWidth) { // Get pixel color from bg color if (filledTexture == false) { tex.SetPixel(x, y, bgColor); } continue; } // Out of decoded data if (dataIndex >= decodedData.Length) { if (filledTexture == false) { tex.SetPixel(x, y, bgColor); if (dataIndex == decodedData.Length) { Debug.LogError("dataIndex exceeded the size of decodedData. dataIndex:" + dataIndex + " decodedData.Length:" + decodedData.Length + " y:" + y + " x:" + x); } } dataIndex++; continue; } // Get pixel color from color table { byte colorIndex = decodedData[dataIndex]; if (colorTable == null || colorTable.Count <= colorIndex) { if (filledTexture == false) { tex.SetPixel(x, y, bgColor); if (colorTable == null) { Debug.LogError("colorIndex exceeded the size of colorTable. colorTable is null. colorIndex:" + colorIndex); } else { Debug.LogError("colorIndex exceeded the size of colorTable. colorTable.Count:" + colorTable.Count + " colorIndex:" + colorIndex); } } dataIndex++; continue; } byte[] rgb = colorTable[colorIndex]; // Set alpha byte alpha = transparentIndex >= 0 && transparentIndex == colorIndex ? (byte)0 : (byte)255; if (filledTexture == false || alpha != 0) { // Set color Color32 col = new Color32(rgb[0], rgb[1], rgb[2], alpha); tex.SetPixel(x, y, col); } } dataIndex++; } } #endregion #region Decode LZW & Sort interrace methods /// /// GIF LZW decode /// /// LZW compressed data /// LZW minimum code size /// Need decoded data size /// Decoded data array private static byte[] DecodeGifLZW(List compData, int lzwMinimumCodeSize, int needDataSize) { int clearCode = 0; int finishCode = 0; // Initialize dictionary Dictionary dic = new Dictionary(); int lzwCodeSize = 0; InitDictionary(dic, lzwMinimumCodeSize, out lzwCodeSize, out clearCode, out finishCode); // Convert to bit array byte[] compDataArr = compData.ToArray(); var bitData = new BitArray(compDataArr); byte[] output = new byte[needDataSize]; int outputAddIndex = 0; string prevEntry = null; bool dicInitFlag = false; int bitDataIndex = 0; // LZW decode loop while (bitDataIndex < bitData.Length) { if (dicInitFlag) { InitDictionary(dic, lzwMinimumCodeSize, out lzwCodeSize, out clearCode, out finishCode); dicInitFlag = false; } int key = bitData.GetNumeral(bitDataIndex, lzwCodeSize); string entry = null; if (key == clearCode) { // Clear (Initialize dictionary) dicInitFlag = true; bitDataIndex += lzwCodeSize; prevEntry = null; continue; } else if (key == finishCode) { // Exit Debug.LogWarning("early stop code. bitDataIndex:" + bitDataIndex + " lzwCodeSize:" + lzwCodeSize + " key:" + key + " dic.Count:" + dic.Count); break; } else if (dic.ContainsKey(key)) { // Output from dictionary entry = dic[key]; } else if (key >= dic.Count) { if (prevEntry != null) { // Output from estimation entry = prevEntry + prevEntry[0]; } else { Debug.LogWarning("It is strange that come here. bitDataIndex:" + bitDataIndex + " lzwCodeSize:" + lzwCodeSize + " key:" + key + " dic.Count:" + dic.Count); bitDataIndex += lzwCodeSize; continue; } } else { Debug.LogWarning("It is strange that come here. bitDataIndex:" + bitDataIndex + " lzwCodeSize:" + lzwCodeSize + " key:" + key + " dic.Count:" + dic.Count); bitDataIndex += lzwCodeSize; continue; } // Output // Take out 8 bits from the string. byte[] temp = Encoding.Unicode.GetBytes(entry); for (int i = 0; i < temp.Length; i++) { if (i % 2 == 0) { output[outputAddIndex] = temp[i]; outputAddIndex++; } } if (outputAddIndex >= needDataSize) { // Exit break; } if (prevEntry != null) { // Add to dictionary dic.Add(dic.Count, prevEntry + entry[0]); } prevEntry = entry; bitDataIndex += lzwCodeSize; if (lzwCodeSize == 3 && dic.Count >= 8) { lzwCodeSize = 4; } else if (lzwCodeSize == 4 && dic.Count >= 16) { lzwCodeSize = 5; } else if (lzwCodeSize == 5 && dic.Count >= 32) { lzwCodeSize = 6; } else if (lzwCodeSize == 6 && dic.Count >= 64) { lzwCodeSize = 7; } else if (lzwCodeSize == 7 && dic.Count >= 128) { lzwCodeSize = 8; } else if (lzwCodeSize == 8 && dic.Count >= 256) { lzwCodeSize = 9; } else if (lzwCodeSize == 9 && dic.Count >= 512) { lzwCodeSize = 10; } else if (lzwCodeSize == 10 && dic.Count >= 1024) { lzwCodeSize = 11; } else if (lzwCodeSize == 11 && dic.Count >= 2048) { lzwCodeSize = 12; } else if (lzwCodeSize == 12 && dic.Count >= 4096) { int nextKey = bitData.GetNumeral(bitDataIndex, lzwCodeSize); if (nextKey != clearCode) { dicInitFlag = true; } } } return output; } /// /// Initialize dictionary /// /// Dictionary /// LZW minimum code size /// out LZW code size /// out Clear code /// out Finish code private static void InitDictionary(Dictionary dic, int lzwMinimumCodeSize, out int lzwCodeSize, out int clearCode, out int finishCode) { int dicLength = (int)Math.Pow(2, lzwMinimumCodeSize); clearCode = dicLength; finishCode = clearCode + 1; dic.Clear(); for (int i = 0; i < dicLength + 2; i++) { dic.Add(i, ((char)i).ToString()); } lzwCodeSize = lzwMinimumCodeSize + 1; } /// /// Sort interlace GIF data /// /// Decoded GIF data /// Pixel number of horizontal row /// Sorted data private static byte[] SortInterlaceGifData(byte[] decodedData, int xNum) { int rowNo = 0; int dataIndex = 0; var newArr = new byte[decodedData.Length]; // Every 8th. row, starting with row 0. for (int i = 0; i < newArr.Length; i++) { if (rowNo % 8 == 0) { newArr[i] = decodedData[dataIndex]; dataIndex++; } if (i != 0 && i % xNum == 0) { rowNo++; } } rowNo = 0; // Every 8th. row, starting with row 4. for (int i = 0; i < newArr.Length; i++) { if (rowNo % 8 == 4) { newArr[i] = decodedData[dataIndex]; dataIndex++; } if (i != 0 && i % xNum == 0) { rowNo++; } } rowNo = 0; // Every 4th. row, starting with row 2. for (int i = 0; i < newArr.Length; i++) { if (rowNo % 4 == 2) { newArr[i] = decodedData[dataIndex]; dataIndex++; } if (i != 0 && i % xNum == 0) { rowNo++; } } rowNo = 0; // Every 2nd. row, starting with row 1. for (int i = 0; i < newArr.Length; i++) { if (rowNo % 8 != 0 && rowNo % 8 != 4 && rowNo % 4 != 2) { newArr[i] = decodedData[dataIndex]; dataIndex++; } if (i != 0 && i % xNum == 0) { rowNo++; } } return newArr; } #endregion }