Compare commits
	
		
			3 Commits
		
	
	
		
			fcf0c6a503
			...
			d3dd40eec3
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | d3dd40eec3 | |
|  | bf475ea21a | |
|  | 5d958fd7ad | 
|  | @ -4,7 +4,4 @@ | ||||||
|     <EnableDynamicLoading>true</EnableDynamicLoading> |     <EnableDynamicLoading>true</EnableDynamicLoading> | ||||||
|     <RootNamespace>ThiefMissionViewer</RootNamespace> |     <RootNamespace>ThiefMissionViewer</RootNamespace> | ||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|   <ItemGroup> |  | ||||||
|     <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> |  | ||||||
|   </ItemGroup> |  | ||||||
| </Project> | </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.IO; | ||||||
| using System.Numerics; | using System.Numerics; | ||||||
|  | using System.Text; | ||||||
| 
 | 
 | ||||||
| namespace KeepersCompound.LGS; | namespace KeepersCompound.LGS; | ||||||
| 
 | 
 | ||||||
|  | @ -14,4 +15,13 @@ public static class Extensions | ||||||
|     { |     { | ||||||
|         return new Vector2(reader.ReadSingle(), reader.ReadSingle()); |         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 Godot; | ||||||
| using SixLabors.ImageSharp.PixelFormats; | using KeepersCompound.Images; | ||||||
| 
 | 
 | ||||||
| namespace KeepersCompound.TMV; | namespace KeepersCompound.TMV; | ||||||
| 
 | 
 | ||||||
| public partial class TextureLoader | public partial class TextureLoader | ||||||
| { | { | ||||||
|     // TODO: Replace this with my own implementation lol |  | ||||||
|     // TODO: Alpha!? |  | ||||||
|     // References: |     // References: | ||||||
|     // - https://www.w3.org/Graphics/GIF/spec-gif89a.txt |     // - https://www.w3.org/Graphics/GIF/spec-gif89a.txt | ||||||
|     private static ImageTexture LoadGif(string path) |     private static ImageTexture LoadGif(string path) | ||||||
|     { |     { | ||||||
|         using var gifImage = SixLabors.ImageSharp.Image.Load<Rgba32>(path); |         var gif = new GifDecoder(path); | ||||||
| 
 |         var gifImage = gif.GetImage(0); | ||||||
|         var width = gifImage.Width; |         var bytes = gifImage.GetRgbaBytes(); | ||||||
|         var height = gifImage.Height; |         var image = Image.CreateFromData(gifImage.Width, gifImage.Height, false, Image.Format.Rgba8, bytes); | ||||||
|         var bytes = new byte[width * height * 4]; |  | ||||||
|         gifImage.CopyPixelDataTo(bytes); |  | ||||||
|         var image = Image.CreateFromData(width, height, false, Image.Format.Rgba8, bytes); |  | ||||||
|         return ImageTexture.CreateFromImage(image); |         return ImageTexture.CreateFromImage(image); | ||||||
|     } |     } | ||||||
| } | } | ||||||
		Loading…
	
		Reference in New Issue