From 2abbe6f3b759dd71796c66af70dfbd1be972397c Mon Sep 17 00:00:00 2001 From: gopicolo Date: Mon, 5 Jan 2026 12:00:03 -0300 Subject: [PATCH] Add PS1 TIM image support --- ArcFormats/Ps1/ImageTIM.cs | 173 +++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 ArcFormats/Ps1/ImageTIM.cs diff --git a/ArcFormats/Ps1/ImageTIM.cs b/ArcFormats/Ps1/ImageTIM.cs new file mode 100644 index 00000000..ad678c57 --- /dev/null +++ b/ArcFormats/Ps1/ImageTIM.cs @@ -0,0 +1,173 @@ +//! \file ImageTIM.cs +//! \date 2026 Jan 05 +//! \brief Standard PlayStation (PS1) image format. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace GameRes.Formats.Sony +{ + internal class TimMetaData : ImageMetaData + { + public uint BppMode; + public bool HasClut; + public uint ClutOffset; + public uint ImageOffset; + } + + [Export(typeof(ImageFormat))] + public class TimFormat : ImageFormat + { + public override string Tag { get { return "TIM"; } } + public override string Description { get { return "PlayStation image format"; } } + public override uint Signature { get { return 0x00000010; } } // Magic: 10 00 00 00 + + public override ImageMetaData ReadMetaData(IBinaryStream stream) + { + var header = stream.ReadHeader(8); + if (header[0] != 0x10 || header[1] != 0) + return null; + + ushort flag = header.ToUInt16(4); + uint mode = (uint)(flag & 3); + bool hasClut = (flag & 8) != 0; + + uint currentOffset = 8; + uint clutOffset = 0; + + if (hasClut) + { + clutOffset = currentOffset; + stream.Position = currentOffset; + uint clutSize = stream.ReadUInt32(); + currentOffset += clutSize; + } + + uint imageOffset = currentOffset; + if (imageOffset + 12 > stream.Length) return null; + + // Image Header: BlockSize(4), VramX(2), VramY(2), WordWidth(2), Height(2) + stream.Position = imageOffset + 8; + ushort wordWidth = stream.ReadUInt16(); + ushort height = stream.ReadUInt16(); + + int bpp; + int width; + switch (mode) + { + case 0: bpp = 4; width = wordWidth * 4; break; + case 1: bpp = 8; width = wordWidth * 2; break; + case 2: bpp = 16; width = wordWidth; break; + case 3: bpp = 24; width = (wordWidth * 2) / 3; break; + default: return null; + } + + return new TimMetaData + { + Width = (uint)width, + Height = height, + BPP = bpp, + BppMode = mode, + HasClut = hasClut, + ClutOffset = clutOffset, + ImageOffset = imageOffset + }; + } + + public override ImageData Read(IBinaryStream stream, ImageMetaData info) + { + var meta = (TimMetaData)info; + BitmapPalette palette = null; + + if (meta.HasClut) + { + // CLUT Header: BlockSize(4), X(2), Y(2), Width(2), Height(2) + // Width/Height are at offsets 8 and 10 within the CLUT block. + stream.Position = meta.ClutOffset + 8; + ushort colorsCount = stream.ReadUInt16(); + ushort rows = stream.ReadUInt16(); + int totalColors = colorsCount * rows; + + // Color data starts at offset 12 + var colorData = stream.ReadBytes(totalColors * 2); + var colors = new Color[totalColors]; + for (int i = 0; i < totalColors; i++) + { + ushort c = BitConverter.ToUInt16(colorData, i * 2); + colors[i] = ConvertBgr1555(c); + } + palette = new BitmapPalette(colors); + } + + // Image data starts at ImageOffset + 12 (skipping the 12-byte header) + stream.Position = meta.ImageOffset + 12; + int pixelDataSize = (int)(meta.Width * meta.Height * meta.BPP / 8); + if (meta.BPP == 4) pixelDataSize = (int)(meta.Width * meta.Height / 2); + + var pixels = stream.ReadBytes(pixelDataSize); + PixelFormat format; + + switch (meta.BppMode) + { + case 0: format = PixelFormats.Indexed4; break; + case 1: format = PixelFormats.Indexed8; break; + case 2: format = PixelFormats.Bgr555; break; + case 3: format = PixelFormats.Bgr24; break; + default: throw new NotSupportedException("Unsupported TIM mode"); + } + + // PS1 4bpp nibbles are stored in reverse order compared to Windows standard + if (meta.BppMode == 0) + { + for (int i = 0; i < pixels.Length; i++) + { + byte b = pixels[i]; + pixels[i] = (byte)((b >> 4) | (b << 4)); + } + } + + return ImageData.Create(info, format, palette, pixels); + } + + public override void Write(Stream file, ImageData image) + { + throw new NotImplementedException(); + } + + private Color ConvertBgr1555(ushort c) + { + // PS1 BGR1555: Bit 15=STP, 14-10=B, 9-5=G, 4-0=R + byte r = (byte)((c & 0x1F) << 3); + byte g = (byte)(((c >> 5) & 0x1F) << 3); + byte b = (byte)(((c >> 10) & 0x1F) << 3); + + // Transparency logic: 0,0,0 is transparent unless STP bit is set. + byte a = (c == 0) ? (byte)0 : (byte)255; + if ((c & 0x8000) != 0) a = 255; + + return Color.FromArgb(a, r, g, b); + } + } +} \ No newline at end of file