From c4d3c7151327713ea221fad67d85930aa913d4aa Mon Sep 17 00:00:00 2001 From: Crsky Date: Fri, 13 Mar 2026 23:58:37 +0800 Subject: [PATCH 1/7] feat: Support TLG mux qoi --- ArcFormats/KiriKiri/ImageTLG.cs | 168 +++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 3 deletions(-) diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index f63f0d52..e21fb77c 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -22,7 +22,7 @@ namespace GameRes.Formats.KiriKiri internal class TlgMetaData : ImageMetaData { public int Version; - public int DataOffset; + public long DataOffset; } [Export(typeof(ImageFormat))] @@ -35,7 +35,7 @@ namespace GameRes.Formats.KiriKiri public TlgFormat () { Extensions = new string[] { "tlg", "tlg5", "tlg6" }; - Signatures = new uint[] { 0x30474C54, 0x35474C54, 0x36474C54, 0x35474CAB, 0x584D4B4A }; + Signatures = new uint[] { 0x30474C54, 0x35474C54, 0x36474C54, 0x35474CAB, 0x584D4B4A, 0x6D474C54 }; } public override ImageMetaData ReadMetaData (IBinaryStream stream) @@ -45,7 +45,7 @@ namespace GameRes.Formats.KiriKiri if (!header.AsciiEqual ("TLG0.0\x00sds\x1a")) offset = 0; int version; - if (!header.AsciiEqual (offset+6, "\x00raw\x1a")) + if (!header.AsciiEqual (offset+6, "\x00raw\x1a") && !header.AsciiEqual (offset+6, "\x00idx\x1a")) return null; if (0xAB == header[offset]) header[offset] = (byte)'T'; @@ -53,6 +53,8 @@ namespace GameRes.Formats.KiriKiri version = 6; else if (header.AsciiEqual (offset, "TLG5.0")) version = 5; + else if (header.AsciiEqual (offset, "TLGmux")) + version = 0; else if (header.AsciiEqual (offset, "XXXYYY")) { version = 5; @@ -137,6 +139,8 @@ namespace GameRes.Formats.KiriKiri src.Position = info.DataOffset; if (6 == info.Version) return ReadV6 (src, info); + else if (0 == info.Version) + return ReadMUX (src, info); else return ReadV5 (src, info); } @@ -1085,6 +1089,164 @@ namespace GameRes.Formats.KiriKiri } } } + + static class QoiCodec + { + public const int Index = 0x00; + public const int Diff = 0x40; + public const int Luma = 0x80; + public const int Run = 0xC0; + public const int Rgb = 0xFE; + public const int Rgba = 0xFF; + public const int Mask2 = 0xC0; + public const int HashTableSize = 64; + } + + byte[] DecodeQOI (IBinaryStream src, uint width, uint height) + { + var output = new byte[4*width*height]; + var table = new byte[4*QoiCodec.HashTableSize]; + byte r = 0, g = 0, b = 0, a = 255; + var run = 0; + for (var dst = 0; dst < output.Length; dst += 4) + { + if (run > 0) + run--; + else + { + var b1 = src.ReadByte (); + if (-1 == b1) + throw new EndOfStreamException (); + if (QoiCodec.Rgb == b1) + { + var rgb = src.ReadInt24 (); + r = (byte)rgb; + g = (byte)(rgb >> 8); + b = (byte)(rgb >> 16); + } + else if (QoiCodec.Rgba == b1) + { + var rgba = src.ReadInt32 (); + r = (byte)rgba; + g = (byte)(rgba >> 8); + b = (byte)(rgba >> 16); + a = (byte)(rgba >> 24); + } + else if (QoiCodec.Index == (b1 & QoiCodec.Mask2)) + { + var p1 = (b1 & ~QoiCodec.Mask2) * 4; + r = table[p1 ]; + g = table[p1+1]; + b = table[p1+2]; + a = table[p1+3]; + } + else if (QoiCodec.Diff == (b1 & QoiCodec.Mask2)) + { + r += (byte)(((b1 >> 4) & 0x03) - 2); + g += (byte)(((b1 >> 2) & 0x03) - 2); + b += (byte)((b1 & 0x03) - 2); + } + else if (QoiCodec.Luma == (b1 & QoiCodec.Mask2)) + { + var b2 = src.ReadByte (); + if (-1 == b2) + throw new EndOfStreamException (); + var vg = (b1 & 0x3F) - 32; + r += (byte)(vg - 8 + ((b2 >> 4) & 0x0F)); + g += (byte)vg; + b += (byte)(vg - 8 + (b2 & 0x0F)); + } + else if (QoiCodec.Run == (b1 & QoiCodec.Mask2)) + { + run = b1 & 0x3F; + } + var p2 = (r*3 + g*5 + b*7 + a*11) % QoiCodec.HashTableSize*4; + table[p2 ] = r; + table[p2+1] = g; + table[p2+2] = b; + table[p2+3] = a; + } + output[dst ] = b; + output[dst+1] = g; + output[dst+2] = r; + output[dst+3] = a; + } + return output; + } + + byte[] ReadQOI (IBinaryStream src, TlgMetaData info) + { + var color_type = src.ReadByte (); + if (3 != color_type && 4 != color_type) + throw new InvalidFormatException (); + var width = src.ReadUInt32 (); + var height = src.ReadUInt32 (); + if (width != info.Width || height != info.Height) + throw new InvalidFormatException (); + while (true) + { + var entry_signature = src.ReadInt32 (); + var entry_size = src.ReadInt32 (); + if (0x52444851 == entry_signature) // 'QHDR' + { + throw new NotImplementedException (); + } + else if (0 == entry_signature && 0 == entry_size) + break; + else + throw new InvalidFormatException (); + } + return DecodeQOI (src, width, height); + } + + byte[] ReadMUX (IBinaryStream src, TlgMetaData info) + { + src.Position = info.DataOffset; + var slices = new List (); + while (true) + { + var entry_signature = src.ReadInt32 (); + var entry_size = src.ReadInt32 (); + if (0x58554D43 == entry_signature) // 'CMUX' + { + var entry = src.ReadBytes (entry_size); + var count = entry.ToInt32 (0); + if (0 == count) + throw new InvalidFormatException (); + var offset = 4; + for (var i = 0; i < count; i++) + { + slices.Add (new TlgMetaData + { + OffsetX = entry.ToInt32 (offset), + OffsetY = entry.ToInt32 (offset+4), + Width = entry.ToUInt32 (offset+8), + Height = entry.ToUInt32 (offset+12), + DataOffset = entry.ToInt64 (offset+16) + }); + offset += 24; + } + } + else if (0 == entry_signature && 0 == entry_size) + break; + else + throw new InvalidFormatException (); + } + var data_offset = src.Position; + var image = new byte[4*info.Width*info.Height]; + foreach (var slice_info in slices) + { + src.Position = data_offset + slice_info.DataOffset; + byte[] slice; + var header = src.ReadBytes (11); + if (header.AsciiEqual (0, "TLGqoi") && header.AsciiEqual (7, "raw")) + slice = ReadQOI (src, slice_info); + else + throw new NotImplementedException (); + BlendImage (image, info, slice, slice_info, 0); + } + return image; + } } internal class TagsParser From cd548330a9f97328f88ef4264097e1dee002bc81 Mon Sep 17 00:00:00 2001 From: Crsky Date: Sat, 14 Mar 2026 02:26:39 +0800 Subject: [PATCH 2/7] feat: Support TLGqoi file --- ArcFormats/KiriKiri/ImageTLG.cs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index e21fb77c..9b6f2b65 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -35,7 +35,7 @@ namespace GameRes.Formats.KiriKiri public TlgFormat () { Extensions = new string[] { "tlg", "tlg5", "tlg6" }; - Signatures = new uint[] { 0x30474C54, 0x35474C54, 0x36474C54, 0x35474CAB, 0x584D4B4A, 0x6D474C54 }; + Signatures = new uint[] { 0x30474C54, 0x35474C54, 0x36474C54, 0x35474CAB, 0x584D4B4A, 0x6D474C54, 0x71474C54 }; } public override ImageMetaData ReadMetaData (IBinaryStream stream) @@ -55,6 +55,8 @@ namespace GameRes.Formats.KiriKiri version = 5; else if (header.AsciiEqual (offset, "TLGmux")) version = 0; + else if (header.AsciiEqual (offset, "TLGqoi")) + version = 1; else if (header.AsciiEqual (offset, "XXXYYY")) { version = 5; @@ -141,6 +143,8 @@ namespace GameRes.Formats.KiriKiri return ReadV6 (src, info); else if (0 == info.Version) return ReadMUX (src, info); + else if (1 == info.Version) + return ReadQOI (src, info); else return ReadV5 (src, info); } @@ -1176,13 +1180,6 @@ namespace GameRes.Formats.KiriKiri byte[] ReadQOI (IBinaryStream src, TlgMetaData info) { - var color_type = src.ReadByte (); - if (3 != color_type && 4 != color_type) - throw new InvalidFormatException (); - var width = src.ReadUInt32 (); - var height = src.ReadUInt32 (); - if (width != info.Width || height != info.Height) - throw new InvalidFormatException (); while (true) { var entry_signature = src.ReadInt32 (); @@ -1196,7 +1193,7 @@ namespace GameRes.Formats.KiriKiri else throw new InvalidFormatException (); } - return DecodeQOI (src, width, height); + return DecodeQOI (src, info.Width, info.Height); } byte[] ReadMUX (IBinaryStream src, TlgMetaData info) @@ -1240,7 +1237,16 @@ namespace GameRes.Formats.KiriKiri byte[] slice; var header = src.ReadBytes (11); if (header.AsciiEqual (0, "TLGqoi") && header.AsciiEqual (7, "raw")) + { + var channels = src.ReadByte (); + var width = src.ReadUInt32 (); + var height = src.ReadUInt32 (); + if (3 != channels && 4 != channels) + throw new InvalidFormatException (); + if (width != slice_info.Width || height != slice_info.Height) + throw new InvalidFormatException (); slice = ReadQOI (src, slice_info); + } else throw new NotImplementedException (); BlendImage (image, info, slice, slice_info, 0); From bb2c831206ed41641308f2a536364469c231eff1 Mon Sep 17 00:00:00 2001 From: Crsky Date: Tue, 17 Mar 2026 09:41:29 +0800 Subject: [PATCH 3/7] feat: Support multi-layer qoi --- ArcFormats/ArcFormats.csproj | 3 + ArcFormats/KiriKiri/ImageTLG.cs | 330 +++++++++++++++++++++++++++++++- ArcFormats/packages.config | 1 + 3 files changed, 333 insertions(+), 1 deletion(-) diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index c19e7d6d..0970026f 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -61,6 +61,9 @@ ..\packages\SharpZipLib.1.4.2\lib\netstandard2.0\ICSharpCode.SharpZipLib.dll + + ..\packages\K4os.Compression.LZ4.1.3.8\lib\net462\K4os.Compression.LZ4.dll + ..\packages\Microsoft.Bcl.HashCode.6.0.0\lib\net462\Microsoft.Bcl.HashCode.dll diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index 9b6f2b65..a715900f 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -16,6 +16,8 @@ using GameRes.Utility; using System.Collections.Generic; using System.Diagnostics; using System.Text; +using K4os.Compression.LZ4; +using System.Threading.Tasks; namespace GameRes.Formats.KiriKiri { @@ -1178,21 +1180,347 @@ namespace GameRes.Formats.KiriKiri return output; } + class Lz4DecodeStream + { + readonly IBinaryStream m_input; + readonly byte[][] m_buffer; + readonly int[] m_size; + int m_index; + int m_pos; + + const int BUFFER_SIZE = 0x8000; + + public Lz4DecodeStream (IBinaryStream input) + { + m_input = input; + m_buffer = new byte[2][]; + m_size = new int[2]; + for (var i = 0; i < m_buffer.Length; i++) + m_buffer[i] = new byte[BUFFER_SIZE]; + } + + bool FillBuffer () + { + if (m_input.Position == m_input.Length) + return false; + var v1 = m_input.ReadUInt16 (); + var v2 = m_input.ReadUInt16 (); + var input = m_input.ReadBytes (v2); + if (v2 != input.Length) + throw new EndOfStreamException (); + var size = v1 & 0x7FFF; + if (0 == size) + size = 0x8000; + var dst = m_index ^ 1; + var dic = m_index; + int num; + if (0 != (v1 & 0x8000)) + { + if (0 == m_size[dic]) + throw new InvalidFormatException (); + num = LZ4Codec.Decode (input, m_buffer[dst], m_buffer[dic].AsSpan (0, m_size[dic])); + } + else + num = LZ4Codec.Decode (input, m_buffer[dst]); + if (-1 == num || size != num) + throw new InvalidFormatException (); + m_size[dst] = num; + m_index ^= 1; + m_pos = 0; + return true; + } + + public int ReadByte () + { + if (m_pos == m_size[m_index]) + { + if (!FillBuffer ()) + return -1; + } + var idx = m_index; + if (m_pos == m_size[idx]) + return -1; + return m_buffer[idx][m_pos++]; + } + } + + class RunDecodeStream + { + readonly Lz4DecodeStream m_input; + + public RunDecodeStream (IBinaryStream input) + { + m_input = new Lz4DecodeStream (input); + } + + public int Read () + { + var value = 0; + var shift = 0; + while (shift < 32) + { + var b = m_input.ReadByte (); + if (-1 == b) + throw new EndOfStreamException (); + value |= (int)(b & 0x7F) << shift; + if (0 == (b & 0x80)) + break; + shift += 7; + } + return value; + } + } + + class QoiDecodeStream + { + readonly IBinaryStream m_input; + readonly byte[] m_table; + uint m_pixel; + + public QoiDecodeStream (IBinaryStream input) + { + m_input = input; + m_table = new byte[4*QoiCodec.HashTableSize]; + m_pixel = 0xFF000000; + } + + public int Read (out uint output) + { + var r = (byte)m_pixel; + var g = (byte)(m_pixel >> 8); + var b = (byte)(m_pixel >> 16); + var a = (byte)(m_pixel >> 24); + var run = 1; + var b1 = m_input.ReadByte (); + if (-1 == b1) + throw new EndOfStreamException (); + if (QoiCodec.Rgb == b1) + { + var rgb = m_input.ReadInt24 (); + r = (byte)rgb; + g = (byte)(rgb >> 8); + b = (byte)(rgb >> 16); + } + else if (QoiCodec.Rgba == b1) + { + var rgba = m_input.ReadInt32 (); + r = (byte)rgba; + g = (byte)(rgba >> 8); + b = (byte)(rgba >> 16); + a = (byte)(rgba >> 24); + } + else if (QoiCodec.Index == (b1 & QoiCodec.Mask2)) + { + var p1 = (b1 & ~QoiCodec.Mask2) * 4; + r = m_table[p1 ]; + g = m_table[p1+1]; + b = m_table[p1+2]; + a = m_table[p1+3]; + } + else if (QoiCodec.Diff == (b1 & QoiCodec.Mask2)) + { + r += (byte)(((b1 >> 4) & 0x03) - 2); + g += (byte)(((b1 >> 2) & 0x03) - 2); + b += (byte)((b1 & 0x03) - 2); + } + else if (QoiCodec.Luma == (b1 & QoiCodec.Mask2)) + { + var b2 = m_input.ReadByte (); + if (-1 == b2) + throw new EndOfStreamException (); + var vg = (b1 & 0x3F) - 32; + r += (byte)(vg - 8 + ((b2 >> 4) & 0x0F)); + g += (byte)vg; + b += (byte)(vg - 8 + (b2 & 0x0F)); + } + else if (QoiCodec.Run == (b1 & QoiCodec.Mask2)) + { + run = (b1 & 0x3F) + 1; + } + var p2 = (r*3 + g*5 + b*7 + a*11) % QoiCodec.HashTableSize*4; + m_table[p2 ] = r; + m_table[p2+1] = g; + m_table[p2+2] = b; + m_table[p2+3] = a; + m_pixel = (uint)(r | (g << 8) | (b << 16) | (a << 24)); + output = (uint)(b | (g << 8) | (r << 16) | (a << 24)); + return run; + } + } + + class QoiBlockDecoder + { + readonly QoiDecodeStream m_qoi; + readonly RunDecodeStream m_run; + readonly int m_pixel_count; + readonly int m_layer_index; + readonly int m_layer_count; + readonly byte[] m_output; + readonly int m_dst; + + public QoiBlockDecoder (byte[] qoi, byte[] run, int pixel_count, int layer_index, int layer_count, byte[] output, int dst) + { + m_qoi = new QoiDecodeStream (new BinMemoryStream (qoi)); + m_run = new RunDecodeStream (new BinMemoryStream (run)); + m_pixel_count = pixel_count; + m_layer_index = layer_index; + m_layer_count = layer_count; + m_output = output; + m_dst = dst; + } + + public void Decode () + { + m_qoi.Read (out var p0); + m_qoi.Read (out var p1); + if (0 != p0 || 0xFF000000 != p1) + throw new InvalidFormatException (); + var r0 = m_run.Read(); + if (0 != r0) + throw new InvalidFormatException (); + var dst = m_dst; + var count = m_pixel_count; + var skip = m_layer_index; + while (count --> 0) + { + var r1 = m_qoi.Read (out var pixel); + var r2 = m_run.Read (); + var run = r1 + r2; + while (run --> 0) + { + if (skip > 0) + --skip; + else + { + skip = m_layer_count-1; + m_output[dst ] = (byte)pixel; + m_output[dst+1] = (byte)(pixel >> 8); + m_output[dst+2] = (byte)(pixel >> 16); + m_output[dst+3] = (byte)(pixel >> 24); + dst += 4; + } + } + } + } + } + + long[] DecodeArray (ReadOnlySpan input) + { + var output = new List (input.Length); + var i = 0; + while (i < input.Length) + { + long value = 0; + var shift = 0; + while (shift < 64) + { + var b = input[i++]; + value |= (long)(b & 0x7F) << shift; + if (0 == (b & 0x80)) + break; + shift += 7; + if (i >= input.Length) + throw new EndOfStreamException (); + } + output.Add (value); + } + return output.ToArray (); + } + + long[] ReadArray (IBinaryStream src, long offset, int signature) + { + src.Position = offset; + if (signature != src.ReadInt32 ()) + throw new InvalidFormatException (); + var length = src.ReadInt32 (); + var input = src.ReadBytes (length); + if (input.Length != length) + throw new EndOfStreamException (); + return DecodeArray (input); + } + + byte[] DecodeMultiLayerQOI (IBinaryStream src, uint width, uint height, byte[] qhdr, int layer) + { + var data_offset = src.Position; + + var layer_count = qhdr.ToInt32 (4); + var block_height = qhdr.ToInt32 (8); + var block_count = qhdr.ToInt32 (12); + var dtbl_offset = qhdr.ToInt64 (24); + var rtbl_offset = qhdr.ToInt64 (32); + + if (layer_count < 1) + throw new InvalidFormatException (); + + if (0 == block_count) + throw new NotImplementedException (); + + long[] dtbl = ReadArray (src, data_offset+dtbl_offset, 0x4C425444); + if (0 == dtbl.Length || dtbl.Length != 1+2*block_count || dtbl[0] != dtbl.Length-1) + throw new InvalidFormatException (); + + long[] rtbl = ReadArray (src, data_offset+rtbl_offset, 0x4C425452); + if (0 == rtbl.Length || rtbl.Length != 1+block_count || rtbl[0] != rtbl.Length-1) + throw new InvalidFormatException (); + + var tasks = new List (block_count); + var output = new byte[4*width*height]; + + var qoi_offset = data_offset; + var run_offset = src.Position; + + for (var i = 0; i < block_count; i++) + { + var qoi_size = (int) dtbl[1+2*i]; + var run_size = (int) rtbl[1+i]; + var num_pixels = (int) dtbl[1+2*i+1]; + + src.Position = qoi_offset; + var qoi = src.ReadBytes (qoi_size); + if (qoi_size != qoi.Length) + throw new EndOfStreamException (); + + src.Position = run_offset; + var run = src.ReadBytes (run_size); + if (run_size != run.Length) + throw new EndOfStreamException (); + + qoi_offset += qoi_size; + run_offset += run_size; + + var dst = block_height*i * 4*(int)width; + + var decoder = new QoiBlockDecoder (qoi, run, num_pixels, layer, layer_count, output, dst); + + var task = Task.Run (() => decoder.Decode ()); + tasks.Add (task); + } + Task.WhenAll (tasks).Wait (); + return output; + } + byte[] ReadQOI (IBinaryStream src, TlgMetaData info) { + var qhdr = Array.Empty (); while (true) { var entry_signature = src.ReadInt32 (); var entry_size = src.ReadInt32 (); if (0x52444851 == entry_signature) // 'QHDR' { - throw new NotImplementedException (); + if (0x30 != entry_size) + throw new InvalidFormatException (); + qhdr = src.ReadBytes (entry_size); + if (qhdr.Length != entry_size) + throw new EndOfStreamException (); } else if (0 == entry_signature && 0 == entry_size) break; else throw new InvalidFormatException (); } + if (0 != qhdr.Length) + return DecodeMultiLayerQOI (src, info.Width, info.Height, qhdr, 0); return DecodeQOI (src, info.Width, info.Height); } diff --git a/ArcFormats/packages.config b/ArcFormats/packages.config index 23cd49d1..65bcc0cf 100644 --- a/ArcFormats/packages.config +++ b/ArcFormats/packages.config @@ -2,6 +2,7 @@ + From 60b0f0bf40d05a646c92f2b721b5aa5888a08740 Mon Sep 17 00:00:00 2001 From: Crsky Date: Tue, 17 Mar 2026 10:44:30 +0800 Subject: [PATCH 4/7] feat: Add TLG mux qoi archive --- ArcFormats/ArcFormats.csproj | 1 + ArcFormats/KiriKiri/ArcTLG.cs | 109 ++++++++++++++++++++++++++++++++ ArcFormats/KiriKiri/ImageTLG.cs | 3 +- 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 ArcFormats/KiriKiri/ArcTLG.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 0970026f..b410cb58 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -204,6 +204,7 @@ + diff --git a/ArcFormats/KiriKiri/ArcTLG.cs b/ArcFormats/KiriKiri/ArcTLG.cs new file mode 100644 index 00000000..a4d7f568 --- /dev/null +++ b/ArcFormats/KiriKiri/ArcTLG.cs @@ -0,0 +1,109 @@ +//! \file ArcTLG.cs +//! \date Tue Mar 17 2026 10:35:55 +//! \brief KiriKiri TLG image implementation. +//--------------------------------------------------------------------------- +// TLGqoi multi-layer image decoder +// +// C# port by crsky +// + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +namespace GameRes.Formats.KiriKiri +{ + internal class TlgLayerEntry : Entry + { + public int Index; + } + + [Export(typeof(ArchiveFormat))] + public class TlgOpener : ArchiveFormat + { + public override string Tag { get { return "TLG"; } } + public override string Description { get { return "KiriKiri game engine image format"; } } + public override uint Signature { get { return 0x71474C54; } } // 'TLGq' + public override bool IsHierarchic { get { return false; } } + public override bool CanWrite { get { return false; } } + + public TlgOpener () + { + Extensions = new string[] { "tlg" }; + } + + public override ArcFile TryOpen (ArcView file) + { + if (!file.View.AsciiEqual (0, "TLGqoi") || !file.View.AsciiEqual (7, "raw")) + return null; + var qhdr = Array.Empty (); + var offset = 0x14; + while (true) + { + var entry_signature = file.View.ReadInt32 (offset); + var entry_size = file.View.ReadInt32 (offset+4); + offset += 8; + if (0x52444851 == entry_signature) // 'QHDR' + { + if (0x30 != entry_size) + return null; + qhdr = file.View.ReadBytes (offset, (uint)entry_size); + if (entry_size != qhdr.Length) + return null; + offset += entry_size; + } + else if (0 == entry_signature && 0 == entry_size) + break; + else + return null; + } + if (0 == qhdr.Length) + return null; + var layer_count = qhdr.ToInt32 (4); + if (layer_count < 1) + return null; + var block_count = qhdr.ToInt32 (12); + if (0 == block_count) + return null; + var dir = new List (layer_count); + for (var i = 0; i < layer_count; i++) + { + dir.Add (new TlgLayerEntry + { + Name = string.Format ("{0}#{1:D3}.tlg", Path.GetFileNameWithoutExtension (file.Name), i), + Size = (uint)file.MaxOffset, + Type = "image", + Index = i, + }); + } + return new ArcFile (file, this, dir); + } + + static readonly ResourceInstance s_TlgFormat = new ResourceInstance ("TLG"); + + public override IImageDecoder OpenImage (ArcFile arc, Entry entry) + { + var layer_entry = entry as TlgLayerEntry; + if (null == layer_entry) + return base.OpenImage (arc, entry); + var input = arc.File.CreateStream (); + try + { + var info = s_TlgFormat.Value.ReadMetaData (input); + if (null == info) + throw new InvalidFormatException (); + if (info is TlgMetaData tlg) + { + tlg.LayerIndex = layer_entry.Index; + } + return new ImageFormatDecoder (input, s_TlgFormat.Value, info); + } + catch + { + input.Dispose (); + throw; + } + } + } +} diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index a715900f..3d533b70 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -25,6 +25,7 @@ namespace GameRes.Formats.KiriKiri { public int Version; public long DataOffset; + public int LayerIndex; } [Export(typeof(ImageFormat))] @@ -1520,7 +1521,7 @@ namespace GameRes.Formats.KiriKiri throw new InvalidFormatException (); } if (0 != qhdr.Length) - return DecodeMultiLayerQOI (src, info.Width, info.Height, qhdr, 0); + return DecodeMultiLayerQOI (src, info.Width, info.Height, qhdr, info.LayerIndex); return DecodeQOI (src, info.Width, info.Height); } From 387d6110ceef72405a7cb12029673ce1fc511314 Mon Sep 17 00:00:00 2001 From: Crsky Date: Tue, 17 Mar 2026 11:05:12 +0800 Subject: [PATCH 5/7] fix: Add argument check --- ArcFormats/KiriKiri/ImageTLG.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index 3d533b70..4c0acb21 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -1456,6 +1456,9 @@ namespace GameRes.Formats.KiriKiri if (0 == block_count) throw new NotImplementedException (); + if (layer >= layer_count) + throw new ArgumentOutOfRangeException (); + long[] dtbl = ReadArray (src, data_offset+dtbl_offset, 0x4C425444); if (0 == dtbl.Length || dtbl.Length != 1+2*block_count || dtbl[0] != dtbl.Length-1) throw new InvalidFormatException (); From be1485a429393cc7960fb1f19a414310b30e76f7 Mon Sep 17 00:00:00 2001 From: Crsky Date: Tue, 17 Mar 2026 17:12:41 +0800 Subject: [PATCH 6/7] perf: Use stream decode in DecodeQOI --- ArcFormats/KiriKiri/ImageTLG.cs | 99 +++++++++------------------------ 1 file changed, 26 insertions(+), 73 deletions(-) diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index 4c0acb21..3dc1542b 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -1097,86 +1097,27 @@ namespace GameRes.Formats.KiriKiri } } - static class QoiCodec - { - public const int Index = 0x00; - public const int Diff = 0x40; - public const int Luma = 0x80; - public const int Run = 0xC0; - public const int Rgb = 0xFE; - public const int Rgba = 0xFF; - public const int Mask2 = 0xC0; - public const int HashTableSize = 64; - } - byte[] DecodeQOI (IBinaryStream src, uint width, uint height) { - var output = new byte[4*width*height]; - var table = new byte[4*QoiCodec.HashTableSize]; - byte r = 0, g = 0, b = 0, a = 255; + var count = 4*width*height; + var output = new byte [count]; + var qoi = new QoiDecodeStream (src); + uint pixel = 0; var run = 0; - for (var dst = 0; dst < output.Length; dst += 4) + var dst = 0; + while (dst < count) { - if (run > 0) - run--; + if (run > 1) + --run; else { - var b1 = src.ReadByte (); - if (-1 == b1) - throw new EndOfStreamException (); - if (QoiCodec.Rgb == b1) - { - var rgb = src.ReadInt24 (); - r = (byte)rgb; - g = (byte)(rgb >> 8); - b = (byte)(rgb >> 16); - } - else if (QoiCodec.Rgba == b1) - { - var rgba = src.ReadInt32 (); - r = (byte)rgba; - g = (byte)(rgba >> 8); - b = (byte)(rgba >> 16); - a = (byte)(rgba >> 24); - } - else if (QoiCodec.Index == (b1 & QoiCodec.Mask2)) - { - var p1 = (b1 & ~QoiCodec.Mask2) * 4; - r = table[p1 ]; - g = table[p1+1]; - b = table[p1+2]; - a = table[p1+3]; - } - else if (QoiCodec.Diff == (b1 & QoiCodec.Mask2)) - { - r += (byte)(((b1 >> 4) & 0x03) - 2); - g += (byte)(((b1 >> 2) & 0x03) - 2); - b += (byte)((b1 & 0x03) - 2); - } - else if (QoiCodec.Luma == (b1 & QoiCodec.Mask2)) - { - var b2 = src.ReadByte (); - if (-1 == b2) - throw new EndOfStreamException (); - var vg = (b1 & 0x3F) - 32; - r += (byte)(vg - 8 + ((b2 >> 4) & 0x0F)); - g += (byte)vg; - b += (byte)(vg - 8 + (b2 & 0x0F)); - } - else if (QoiCodec.Run == (b1 & QoiCodec.Mask2)) - { - run = b1 & 0x3F; - } - var p2 = (r*3 + g*5 + b*7 + a*11) % QoiCodec.HashTableSize*4; - table[p2 ] = r; - table[p2+1] = g; - table[p2+2] = b; - table[p2+3] = a; + run = qoi.Read (out pixel); } - output[dst ] = b; - output[dst+1] = g; - output[dst+2] = r; - output[dst+3] = a; + output[dst ] = (byte)pixel; + output[dst+1] = (byte)(pixel >> 8); + output[dst+2] = (byte)(pixel >> 16); + output[dst+3] = (byte)(pixel >> 24); + dst += 4; } return output; } @@ -1272,6 +1213,18 @@ namespace GameRes.Formats.KiriKiri } } + static class QoiCodec + { + public const int Index = 0x00; + public const int Diff = 0x40; + public const int Luma = 0x80; + public const int Run = 0xC0; + public const int Rgb = 0xFE; + public const int Rgba = 0xFF; + public const int Mask2 = 0xC0; + public const int HashTableSize = 64; + } + class QoiDecodeStream { readonly IBinaryStream m_input; From bbd7476b6ffe13b5184b1ac534c53f32263dfa12 Mon Sep 17 00:00:00 2001 From: Crsky Date: Tue, 17 Mar 2026 17:29:53 +0800 Subject: [PATCH 7/7] style: format --- ArcFormats/KiriKiri/ImageTLG.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ArcFormats/KiriKiri/ImageTLG.cs b/ArcFormats/KiriKiri/ImageTLG.cs index 3dc1542b..2ebaf1a4 100644 --- a/ArcFormats/KiriKiri/ImageTLG.cs +++ b/ArcFormats/KiriKiri/ImageTLG.cs @@ -1234,7 +1234,7 @@ namespace GameRes.Formats.KiriKiri public QoiDecodeStream (IBinaryStream input) { m_input = input; - m_table = new byte[4*QoiCodec.HashTableSize]; + m_table = new byte [4*QoiCodec.HashTableSize]; m_pixel = 0xFF000000; } @@ -1329,7 +1329,7 @@ namespace GameRes.Formats.KiriKiri m_qoi.Read (out var p1); if (0 != p0 || 0xFF000000 != p1) throw new InvalidFormatException (); - var r0 = m_run.Read(); + var r0 = m_run.Read (); if (0 != r0) throw new InvalidFormatException (); var dst = m_dst; @@ -1515,7 +1515,7 @@ namespace GameRes.Formats.KiriKiri throw new InvalidFormatException (); } var data_offset = src.Position; - var image = new byte[4*info.Width*info.Height]; + var image = new byte [4*info.Width*info.Height]; foreach (var slice_info in slices) { src.Position = data_offset + slice_info.DataOffset;