Compare commits
	
		
			3 Commits
		
	
	
		
			fcf0c6a503
			...
			d3dd40eec3
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | d3dd40eec3 | |
|  | bf475ea21a | |
|  | 5d958fd7ad | 
|  | @ -4,7 +4,4 @@ | |||
|     <EnableDynamicLoading>true</EnableDynamicLoading> | ||||
|     <RootNamespace>ThiefMissionViewer</RootNamespace> | ||||
|   </PropertyGroup> | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
|  | @ -0,0 +1,349 @@ | |||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Numerics; | ||||
| using System.Text; | ||||
| using KeepersCompound.LGS; | ||||
| 
 | ||||
| namespace KeepersCompound.Images; | ||||
| 
 | ||||
| public class GifDecoder | ||||
| { | ||||
|     // TODO: I can drop all these structs and shit lol | ||||
|     // TODO: This fails somewhat on the texture for model "skull". CBA to workout why now | ||||
| 
 | ||||
|     private record Header | ||||
|     { | ||||
|         public string Signature; | ||||
|         public ushort LogicalScreenWidth; | ||||
|         public ushort LogicalScreenHeight; | ||||
|         public bool HasGlobalColorTable; | ||||
|         public byte BitsPerColorChannel; // Seemingly unused lol | ||||
|         public bool IsGlobalColorTableSorted; | ||||
|         public int GlobalColorTableSize; | ||||
|         public byte BackgroundColorIndex; | ||||
|         public float PixelAspectRatio; // Ratio of width over height | ||||
| 
 | ||||
|         public Header(BinaryReader reader) | ||||
|         { | ||||
|             Signature = reader.ReadNullString(6); | ||||
|             if (Signature != "GIF87a" && Signature != "GIF89a") | ||||
|             { | ||||
|                 throw new InvalidDataException("File signature does not match GIF spec"); | ||||
|             } | ||||
|             LogicalScreenWidth = reader.ReadUInt16(); | ||||
|             LogicalScreenHeight = reader.ReadUInt16(); | ||||
|             var flags = reader.ReadByte(); | ||||
|             HasGlobalColorTable = ((flags >> 7) & 0x1) != 0; | ||||
|             // Godot.GD.Print($"HALLO WE HAVE GLOBAL TABLE? {HasGlobalColorTable}"); | ||||
|             BitsPerColorChannel = (byte)(((flags >> 4) & 0x7) + 1); | ||||
|             IsGlobalColorTableSorted = ((flags >> 3) & 0x1) != 0; | ||||
|             GlobalColorTableSize = (int)Math.Pow(2, (flags & 0x7) + 1); | ||||
|             // Godot.GD.Print($"Global colour size? {GlobalColorTableSize}"); | ||||
|             BackgroundColorIndex = reader.ReadByte(); | ||||
|             var rawAspectRatio = reader.ReadByte(); | ||||
|             if (rawAspectRatio == 0) | ||||
|             { | ||||
|                 PixelAspectRatio = 1.0f; | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 PixelAspectRatio = (15 + rawAspectRatio) / 64.0f; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private record GraphicsControl | ||||
|     { | ||||
|         public int TransparencyIndex; | ||||
| 
 | ||||
|         public GraphicsControl(BinaryReader reader) | ||||
|         { | ||||
|             var blockSize = reader.ReadByte(); | ||||
|             if (blockSize != 4) | ||||
|             { | ||||
|                 throw new InvalidDataException($"Graphics Control block size should be 4. Found: {blockSize}"); | ||||
|             } | ||||
| 
 | ||||
|             // The only flag we care about is transparency | ||||
|             var flags = reader.ReadByte(); | ||||
|             var transparency = (flags & 0x1) != 0; | ||||
|             var delayTime = reader.ReadUInt16(); // I don't think thief uses any animated gifs so we'll ignore this :) | ||||
|             var rawTransparencyIndex = reader.ReadByte(); | ||||
|             TransparencyIndex = transparency ? rawTransparencyIndex : -1; | ||||
|             var terminator = reader.ReadByte(); | ||||
|             if (terminator != 0) | ||||
|             { | ||||
|                 throw new InvalidDataException($"Graphics Control terminator should be 0. Found: {terminator}"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public record ImageData | ||||
|     { | ||||
|         public int Width; | ||||
|         public int Height; | ||||
|         private byte[] _colors; | ||||
|         private byte[] _pixelIndices; | ||||
|         private int _transparentIndex; | ||||
| 
 | ||||
|         public ImageData(BinaryReader reader, byte[] globalColors, int transparentIndex) | ||||
|         { | ||||
|             var x = reader.ReadUInt16(); | ||||
|             var y = reader.ReadUInt16(); | ||||
|             var width = reader.ReadUInt16(); | ||||
|             var height = reader.ReadUInt16(); | ||||
|             var flags = reader.ReadByte(); | ||||
|             var hasLocalColorTable = ((flags >> 7) & 0x1) != 0; | ||||
|             var interlaced = ((flags >> 6) & 0x1) != 0; // See appendix E for interlacing | ||||
|             var isLocalColorTableSorted = ((flags >> 5) & 0x1) != 0; | ||||
|             var sizeOfLocalColorTable = (byte)Math.Pow(2, (flags & 0x7) + 1); | ||||
|             byte[] colors; | ||||
|             if (hasLocalColorTable) | ||||
|             { | ||||
|                 colors = ReadColorTable(reader, sizeOfLocalColorTable); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 colors = globalColors; | ||||
|             } | ||||
| 
 | ||||
|             // Now is the fun part. All the lovely LZW encoded pixel data :) | ||||
|             var outIndex = 0; | ||||
|             var pixelIndices = new byte[width * height]; | ||||
| 
 | ||||
|             var minCodeSize = reader.ReadByte(); | ||||
|             var clearCode = 1 << minCodeSize; | ||||
|             var endCode = 1 + (1 << minCodeSize); | ||||
|             var table = new List<byte[]>(); | ||||
| 
 | ||||
|             void ResetTable() | ||||
|             { | ||||
|                 table.Clear(); | ||||
|                 var len = 1 << minCodeSize; | ||||
|                 for (var i = 0; i < len; i++) | ||||
|                 { | ||||
|                     table.Add([(byte)i]); | ||||
|                 } | ||||
|                 table.Add([]); // clear code | ||||
|                 table.Add([]); // end code | ||||
|             } | ||||
| 
 | ||||
|             ResetTable(); | ||||
| 
 | ||||
|             // Remember all this data is in blocks!!! | ||||
|             var compressedBytes = new List<byte>(); | ||||
|             var blockSize = reader.ReadByte(); | ||||
|             while (blockSize != 0) | ||||
|             { | ||||
|                 var bytes = reader.ReadBytes(blockSize); | ||||
|                 compressedBytes.AddRange(bytes); | ||||
|                 blockSize = reader.ReadByte(); | ||||
|             } | ||||
|             // Godot.GD.Print($"End of image data: {reader.BaseStream.Position}"); | ||||
| 
 | ||||
|             using MemoryStream compressedStream = new(compressedBytes.ToArray()); | ||||
|             using BinaryReader compressedReader = new(compressedStream, Encoding.UTF8, false); | ||||
| 
 | ||||
|             var codeSize = minCodeSize + 1; | ||||
|             var codeInputByte = compressedReader.ReadByte(); | ||||
|             var codeInputBit = 0; | ||||
|             var previousCode = -1; | ||||
|             while (true) | ||||
|             { | ||||
|                 // Codes are variable length so we need to manage the bytes | ||||
|                 var code = 0; | ||||
|                 var codeBit = 0; | ||||
|                 while (codeBit < codeSize) | ||||
|                 { | ||||
|                     var codeBitsLeft = codeSize - codeBit; | ||||
|                     var inputBitsLeft = 8 - codeInputBit; | ||||
|                     if (inputBitsLeft == 0) | ||||
|                     { | ||||
|                         codeInputByte = compressedReader.ReadByte(); | ||||
|                         codeInputBit = 0; | ||||
|                         inputBitsLeft = 8; | ||||
|                     } | ||||
|                     var bitsToRead = Math.Min(inputBitsLeft, codeBitsLeft); | ||||
|                     code += ((codeInputByte >> codeInputBit) & ((1 << bitsToRead) - 1)) << codeBit; | ||||
|                     codeBit += bitsToRead; | ||||
|                     codeInputBit += bitsToRead; | ||||
|                 } | ||||
| 
 | ||||
|                 // Match the code | ||||
|                 var codeCount = table.Count; | ||||
|                 if (code == clearCode) | ||||
|                 { | ||||
|                     ResetTable(); | ||||
|                     codeSize = minCodeSize + 1; | ||||
|                     previousCode = -1; | ||||
|                 } | ||||
|                 else if (code == endCode) | ||||
|                 { | ||||
|                     // Godot.GD.Print($"CompressedBytesLeft: {compressedReader.BaseStream.Length - compressedReader.BaseStream.Position}"); | ||||
|                     break; | ||||
|                 } | ||||
|                 else if (code < codeCount) | ||||
|                 { | ||||
|                     // Write code W | ||||
|                     var bytes = table[code]; | ||||
|                     foreach (var b in bytes) | ||||
|                     { | ||||
|                         pixelIndices[outIndex++] = b; | ||||
|                     } | ||||
|                     // Add new code = previous code + first "symbol" of W | ||||
|                     if (previousCode != -1) | ||||
|                     { | ||||
|                         var previousBytes = table[previousCode]; | ||||
|                         var newCodeBytes = new byte[previousBytes.Length + 1]; | ||||
|                         var newCodeBytesIdx = 0; | ||||
|                         foreach (var b in previousBytes) | ||||
|                         { | ||||
|                             newCodeBytes[newCodeBytesIdx++] = b; | ||||
|                         } | ||||
|                         newCodeBytes[newCodeBytesIdx] = bytes[0]; | ||||
|                         table.Add(newCodeBytes); | ||||
|                     } | ||||
|                     previousCode = code; | ||||
|                 } | ||||
|                 else if (code == codeCount) | ||||
|                 { | ||||
|                     // Add new code = previous code + first symbol of previous code | ||||
|                     var previousBytes = table[previousCode]; | ||||
|                     var bytes = new byte[previousBytes.Length + 1]; | ||||
|                     var bytesIdx = 0; | ||||
|                     foreach (var b in previousBytes) | ||||
|                     { | ||||
|                         bytes[bytesIdx++] = b; | ||||
|                     } | ||||
|                     bytes[bytesIdx] = previousBytes[0]; | ||||
|                     table.Add(bytes); | ||||
|                     // Write new code | ||||
|                     foreach (var b in bytes) | ||||
|                     { | ||||
|                         pixelIndices[outIndex++] = b; | ||||
|                     } | ||||
|                     previousCode = code; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw new InvalidDataException("Code out of range"); | ||||
|                 } | ||||
| 
 | ||||
|                 // Increase codesize :) | ||||
|                 if (table.Count == (1 << codeSize)) | ||||
|                 { | ||||
|                     if (codeSize < 12) | ||||
|                     { | ||||
|                         codeSize += 1; | ||||
|                     } | ||||
|                     // else | ||||
|                     // { | ||||
|                     //     // pretty sure this is an error | ||||
|                     //     throw new InvalidDataException("Code Size exceeding 12 bits"); | ||||
|                     // } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             Width = width; | ||||
|             Height = height; | ||||
|             _colors = colors; | ||||
|             _pixelIndices = pixelIndices; | ||||
|             _transparentIndex = transparentIndex; | ||||
|         } | ||||
| 
 | ||||
|         public byte[] GetRgbaBytes() | ||||
|         { | ||||
|             var bytesIdx = 0; | ||||
|             var bytes = new byte[Width * Height * 4]; | ||||
|             foreach (var colorIndex in _pixelIndices) | ||||
|             { | ||||
|                 bytes[bytesIdx++] = _colors[colorIndex * 3]; | ||||
|                 bytes[bytesIdx++] = _colors[colorIndex * 3 + 1]; | ||||
|                 bytes[bytesIdx++] = _colors[colorIndex * 3 + 2]; | ||||
|                 // Fuck you LGS hardcoding the transparent index suck my balls | ||||
|                 // bytes[bytesIdx++] = (byte)((colorIndex == _transparentIndex) ? 0 : 255); | ||||
|                 bytes[bytesIdx++] = (byte)((colorIndex == 0) ? 0 : 255); | ||||
|             } | ||||
|             return bytes; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Header _header; | ||||
|     byte[] _globalColors; | ||||
|     ImageData[] _images; | ||||
| 
 | ||||
|     public GifDecoder(string path) | ||||
|     { | ||||
|         if (!File.Exists(path)) | ||||
|         { | ||||
|             throw new FileNotFoundException(); | ||||
|         } | ||||
| 
 | ||||
|         // Godot.GD.Print($"Decoding image at: {path}"); | ||||
| 
 | ||||
|         using MemoryStream stream = new(File.ReadAllBytes(path)); | ||||
|         using BinaryReader reader = new(stream, Encoding.UTF8, false); | ||||
| 
 | ||||
|         _header = new Header(reader); | ||||
|         _globalColors = ReadColorTable(reader, _header.HasGlobalColorTable ? _header.GlobalColorTableSize : 0); | ||||
|         var images = new List<ImageData>(); | ||||
|         var transparentIndex = -1; | ||||
|         while (true) | ||||
|         { | ||||
|             var id = reader.ReadByte(); | ||||
|             var eof = false; | ||||
|             switch (id) | ||||
|             { | ||||
|                 case 0x2C: // Image | ||||
|                     // Godot.GD.Print("Adding new image!!"); | ||||
|                     images.Add(new ImageData(reader, _globalColors, transparentIndex)); | ||||
|                     transparentIndex = -1; | ||||
|                     break; | ||||
|                 case 0x21: // Extension | ||||
|                     var extId = reader.ReadByte(); | ||||
|                     if (extId == 0xF9) | ||||
|                     { | ||||
|                         var graphicsControl = new GraphicsControl(reader); | ||||
|                         transparentIndex = graphicsControl.TransparencyIndex; | ||||
|                         // Godot.GD.Print($"We set the transparent index: {transparentIndex} for {path}"); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         // We don't support Comment (0xFE), Text (0x01), or Application (0xFF) extensions | ||||
|                         throw new InvalidDataException($"Unknown or unsupported extension identifier in GIF file: {extId}"); | ||||
|                     } | ||||
|                     break; | ||||
|                 case 0x3B: | ||||
|                     eof = true; | ||||
|                     break; | ||||
|                 default: | ||||
|                     throw new InvalidDataException($"Unknown block identifier in GIF file: {id} at {stream.Position}"); | ||||
|             } | ||||
| 
 | ||||
|             if (eof) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         _images = images.ToArray(); | ||||
|     } | ||||
| 
 | ||||
|     public ImageData GetImage(int idx) | ||||
|     { | ||||
|         return _images[idx]; | ||||
|     } | ||||
| 
 | ||||
|     private static byte[] ReadColorTable(BinaryReader reader, int length) | ||||
|     { | ||||
|         var colors = new byte[length * 3]; | ||||
|         for (var i = 0; i < length; i++) | ||||
|         { | ||||
|             colors[i * 3] = reader.ReadByte(); | ||||
|             colors[i * 3 + 1] = reader.ReadByte(); | ||||
|             colors[i * 3 + 2] = reader.ReadByte(); | ||||
|         } | ||||
|         return colors; | ||||
|     } | ||||
| } | ||||
|  | @ -1,5 +1,6 @@ | |||
| using System.IO; | ||||
| using System.Numerics; | ||||
| using System.Text; | ||||
| 
 | ||||
| namespace KeepersCompound.LGS; | ||||
| 
 | ||||
|  | @ -14,4 +15,13 @@ public static class Extensions | |||
|     { | ||||
|         return new Vector2(reader.ReadSingle(), reader.ReadSingle()); | ||||
|     } | ||||
| 
 | ||||
|     // TODO: Go through and replace all usages of string reading with this | ||||
|     public static string ReadNullString(this BinaryReader reader, int length) | ||||
|     { | ||||
|         var tmpName = Encoding.UTF8.GetString(reader.ReadBytes(length)); | ||||
|         var idx = tmpName.IndexOf('\0'); | ||||
|         if (idx >= 0) tmpName = tmpName[..idx]; | ||||
|         return tmpName; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,23 +1,18 @@ | |||
| using Godot; | ||||
| using SixLabors.ImageSharp.PixelFormats; | ||||
| using KeepersCompound.Images; | ||||
| 
 | ||||
| namespace KeepersCompound.TMV; | ||||
| 
 | ||||
| public partial class TextureLoader | ||||
| { | ||||
|     // TODO: Replace this with my own implementation lol | ||||
|     // TODO: Alpha!? | ||||
|     // References: | ||||
|     // - https://www.w3.org/Graphics/GIF/spec-gif89a.txt | ||||
|     private static ImageTexture LoadGif(string path) | ||||
|     { | ||||
|         using var gifImage = SixLabors.ImageSharp.Image.Load<Rgba32>(path); | ||||
| 
 | ||||
|         var width = gifImage.Width; | ||||
|         var height = gifImage.Height; | ||||
|         var bytes = new byte[width * height * 4]; | ||||
|         gifImage.CopyPixelDataTo(bytes); | ||||
|         var image = Image.CreateFromData(width, height, false, Image.Format.Rgba8, bytes); | ||||
|         var gif = new GifDecoder(path); | ||||
|         var gifImage = gif.GetImage(0); | ||||
|         var bytes = gifImage.GetRgbaBytes(); | ||||
|         var image = Image.CreateFromData(gifImage.Width, gifImage.Height, false, Image.Format.Rgba8, bytes); | ||||
|         return ImageTexture.CreateFromImage(image); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue