Compare commits
No commits in common. "d3dd40eec3712930bcdebee22e939e0ff0f51fc9" and "fcf0c6a503b1092ea93024858781ed095b71fd55" have entirely different histories.
d3dd40eec3
...
fcf0c6a503
|
@ -4,4 +4,7 @@
|
|||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<RootNamespace>ThiefMissionViewer</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -1,349 +0,0 @@
|
|||
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,6 +1,5 @@
|
|||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace KeepersCompound.LGS;
|
||||
|
||||
|
@ -15,13 +14,4 @@ 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,18 +1,23 @@
|
|||
using Godot;
|
||||
using KeepersCompound.Images;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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);
|
||||
return ImageTexture.CreateFromImage(image);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue