using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; namespace OpenVIII { /// /// Playstation TIM Texture format. /// /// /// /// /// /// /// upgraded TIM class, because that first one is a trash public class TIM2 : Texture_Base { #region Fields /// /// Bits per pixel /// protected sbyte BPP = -1; /// /// Raw Data buffer /// protected byte[] Buffer; /// /// Image has a CLUT /// protected bool CLP; /// /// Texture Data /// protected TextureData Texture; /// /// Start of Image Data /// protected uint TextureDataPointer; protected bool ThrowExec = true; /// /// Start of Tim Data /// protected uint TIMOffset; protected bool TrimExcess; #endregion Fields #region Constructors /// /// Initialize TIM class /// /// Raw Data buffer /// Start of Tim Data public TIM2(byte[] buffer, uint offset = 0, bool noExc = false) { ThrowExec = !noExc; _Init(buffer, offset); } /// Initialize TIM class BinaryReader /// pointing to the file data Start of Tim Data public TIM2(BinaryReader br, uint offset = 0, bool noExec = false) { ThrowExec = !noExec; _Init(br, offset); } protected TIM2() { } #endregion Constructors #region Properties /// /// Gets Bits per pixel /// public override byte GetBytesPerPixel => (byte)BPP; /// /// Number of clut color palettes /// public override int GetClutCount => Texture.NumOfCluts; /// /// Gets size of clut data as in TIM file /// public override int GetClutSize => Texture.ClutDataSize; /// /// Gets number of colors per palette /// public override int GetColorsCountPerPalette => Texture.NumOfColors; /// /// Height /// public override int GetHeight => Texture.Height; /// /// Gets origin texture coordinate X for VRAM buffer /// public override int GetOrigX => Texture.ImageOrgX; /// /// Gets origin texture coordinate Y for VRAM buffer /// public override int GetOrigY => Texture.ImageOrgY; /// /// Width /// public override int GetWidth => Texture.Width; public bool IgnoreAlpha { get; set; } public bool NotTIM { get; protected set; } #endregion Properties #region Methods public bool Assert(bool a) { if (!a) { NotTIM = true; if (ThrowExec) { throw new InvalidDataException("Invalid TIM File"); } //else // Debug.Assert(a); } return !a; } public override void ForceSetClutColors(ushort newNumOfColors) => Texture.NumOfColors = newNumOfColors; public override void ForceSetClutCount(ushort newClut) => Texture.NumOfCluts = newClut; public override Color[] GetClutColors(ushort clut) { using (var br = new BinaryReader(new MemoryStream(Buffer))) { return GetClutColors(br, clut); } } /// /// Gets Color[] palette from TIM image data /// /// clut index /// public Color[] GetPalette(ushort clut = 0) { Color[] colors; using (var br = new BinaryReader(new MemoryStream(Buffer))) colors = GetClutColors(br, clut); return colors; } /// /// Create Texture from Tim image data. /// /// Active clut data /// /// If true skip size check useful for files with more than just Tim /// /// Texture2D public override Texture2D GetTexture(ushort clut) { using (var br = new BinaryReader(new MemoryStream(Buffer))) { return GetTexture(br, !CLP ? null : GetClutColors(br, clut)); } } public override Texture2D GetTexture() { using (var br = new BinaryReader(new MemoryStream(Buffer))) { return GetTexture(br, !CLP ? null : GetClutColors(br, 0)); } } public override Texture2D GetTexture(Color[] colors) { using (var br = new BinaryReader(new MemoryStream(Buffer))) { if (Assert(CLP) || Assert(colors.Length == Texture.NumOfColors)) return null; return GetTexture(br, colors); } } /// /// Initialize TIM class /// /// BinaryReader pointing to the file data /// Start of Tim Data public void Init(BinaryReader br, uint offset) { br.BaseStream.Seek(offset, SeekOrigin.Begin); if (Assert(br.ReadByte() == 0x10) || //tag Assert(br.ReadByte() == 0)) // version return; br.BaseStream.Seek(2, SeekOrigin.Current); var b = (Bppflag)br.ReadByte(); TIMOffset = offset; if ((b & (Bppflag._24bpp)) >= (Bppflag._24bpp)) BPP = 24; else if ((b & Bppflag._16bpp) != 0) BPP = 16; else if ((b & Bppflag._8bpp) != 0) BPP = 8; else BPP = 4; CLP = (b & Bppflag.CLP) != 0; if (Assert(((BPP == 4 || BPP == 8) && CLP) || ((BPP == 16 || BPP == 24) && !CLP))) return; ReadParameters(br); } public override void Load(byte[] buffer, uint offset = 0) => _Init(buffer, offset); /// /// Writes the Tim file to the hard drive. /// /// Path where you want file to be saved. public override void Save(string path) { using (var bw = new BinaryWriter(File.Create(path))) { bw.Write(TrimExcess ? Buffer : Buffer.Skip((int)TIMOffset).Take((int)(Texture.ImageDataSize + TextureDataPointer)).ToArray()); } } public override void SaveCLUT(string path) { if (!CLP) return; using (var br = new BinaryReader(new MemoryStream(Buffer))) using (var clut = new Texture2D(Memory.Graphics.GraphicsDevice, Texture.NumOfColors, Texture.NumOfCluts)) { for (ushort i = 0; i < Texture.NumOfCluts; i++) { clut.SetData(0, new Rectangle(0, i, Texture.NumOfColors, 1), GetClutColors(br, i), 0, Texture.NumOfColors); } Extended.Save_As_PNG(clut, path, Texture.NumOfColors, Texture.NumOfCluts); } } [SuppressMessage("ReSharper", "UnusedMember.Global")] public override void SavePNG(string path, short clut = -1) { if (Buffer == null || Memory.Graphics == null) return; if (clut == -1) { if (Texture.NumOfCluts > 0) { Enumerable.Range(0, Texture.NumOfCluts).ForEach(x => SavePNG(path, (short)x)); return; } clut = 0; } using (var br = new BinaryReader(new MemoryStream(Buffer))) using (var tex = GetTexture(br, !CLP ? null : GetClutColors(br, (ushort) clut))) if (tex != null) Extended.Save_As_PNG(tex, $"{path}{(CLP && Texture.NumOfCluts > 1 ? "_" + clut.ToString("D") : "")}.png", GetWidth, GetHeight); } protected void _Init(byte[] buffer, uint offset = 0) { Buffer = buffer; using (var br = new BinaryReader(new MemoryStream(buffer))) { Init(br, offset); } } protected void _Init(BinaryReader br, uint offset) { TrimExcess = true; br.BaseStream.Seek(offset, SeekOrigin.Begin); Buffer = br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position)); using (var br2 = new BinaryReader(new MemoryStream(Buffer))) { Init(br2, 0); } } /// /// Output 32 bit Color data for image. /// /// Binary reader pointing to memory stream of data. /// Color[] palette /// /// If true skip size check useful for files with more than just Tim /// /// Color[] /// /// This allows null palette but it doesn't seem to handle the palette being null /// protected TextureBuffer CreateImageBuffer(BinaryReader br, Color[] palette = null) { br.BaseStream.Seek(TextureDataPointer, SeekOrigin.Begin); var buffer = new TextureBuffer(Texture.Width, Texture.Height); //ARGB if (Assert((buffer.Length / BPP) <= br.BaseStream.Length - br.BaseStream.Position)) //make sure the buffer is large enough return null; if (BPP == 8) { for (var i = 0; i < buffer.Length; i++) { var colorKey = br.ReadByte(); if (colorKey < Texture.NumOfColors) buffer[i] = palette[colorKey]; //color key //else // buffer[i] = Color.TransparentBlack; // trying something out of ordinary. } } else if (BPP == 4) { for (var i = 0; i < buffer.Length; i++) { var colorKey = br.ReadByte(); buffer[i] = palette[colorKey & 0xf]; buffer[++i] = palette[colorKey >> 4]; } } else if (BPP == 16) //copied from overture { for (var i = 0; i < buffer.Length && br.BaseStream.Position + 2 < br.BaseStream.Length; i++) buffer[i] = ABGR1555toRGBA32bit(br.ReadUInt16(), IgnoreAlpha); } else if (BPP == 24) //could be wrong. // assuming it is BGR { for (var i = 0; i < buffer.Length && br.BaseStream.Position + 2 < br.BaseStream.Length; i++) { var pixel = br.ReadBytes(3); buffer[i] = new Color { R = pixel[2], G = pixel[1], B = pixel[0], A = 0xFF }; } } else throw new Exception($"TIM_v2::CreateImageBuffer::TIM unsupported bits per pixel = {BPP}"); //Then in bs debug where ReadTexture store for all cluts //data and then create Texture2D from there. (array of e.g. 15 texture2D) return buffer; } /// /// Get clut color palette /// /// Binary reader pointing to memory stream of data. /// Active clut data /// Color[] protected Color[] GetClutColors(BinaryReader br, ushort clut) { if (clut >= Texture.NumOfCluts) throw new Exception($"TIM_v2::GetClutColors::given clut {clut} is >= texture number of cluts {Texture.NumOfCluts}"); if (CLP) { var colorPixels = new Color[Texture.NumOfColors]; br.BaseStream.Seek(TIMOffset + 20 + (Texture.NumOfColors * 2 * clut), SeekOrigin.Begin); for (var i = 0; i < Texture.NumOfColors; i++) colorPixels[i] = ABGR1555toRGBA32bit(br.ReadUInt16(), IgnoreAlpha); return colorPixels; } else throw new Exception($"TIM that has {BPP} bpp mode and has no clut data!"); } protected Texture2D GetTexture(BinaryReader br, Color[] colors) => CreateImageBuffer(br, colors).GetTexture(); /// /// Populate Texture structure /// /// Binary reader pointing to memory stream of data. /// bits per pixel protected void ReadParameters(BinaryReader br) { Texture = new TextureData(); Texture.Read(br, (byte)BPP, CLP, !ThrowExec); if (Assert(!Texture.NotTIM)) return; TextureDataPointer = (uint)br.BaseStream.Position; if (TrimExcess) Buffer = Buffer.Skip((int)TIMOffset).Take((int)(Texture.ImageDataSize + TextureDataPointer - TIMOffset)).ToArray(); } #endregion Methods #region Structs protected struct TextureData { #region Fields public byte[] ClutData; public int ClutDataSize; /// /// length, in bytes, of the entire CLUT block (including the header) /// public uint ClutSize; public ushort Height; public int ImageDataSize; public ushort ImageOrgX; public ushort ImageOrgY; public uint ImageSize; public ushort NumOfCluts; public ushort NumOfColors; public ushort PaletteX; public ushort PaletteY; public bool ThrowExec; public ushort Width; #endregion Fields #region Properties public bool NotTIM { get; private set; } #endregion Properties #region Methods public bool Assert(bool a) { if (!a) { NotTIM = true; if (ThrowExec) { throw new InvalidDataException("Invalid TIM File"); } //else // Debug.Assert(a); } return !a; } /// /// Populate Texture structure /// /// Binary reader pointing to memory stream of data. /// bits per pixel public void Read(BinaryReader br, byte bpp, bool clp, bool noExec = false) { ThrowExec = !noExec; br.BaseStream.Seek(3, SeekOrigin.Current); if (clp) { //long start = br.BaseStream.Position; ClutSize = br.ReadUInt32(); PaletteX = br.ReadUInt16(); PaletteY = br.ReadUInt16(); NumOfColors = br.ReadUInt16(); //width of clut NumOfCluts = br.ReadUInt16(); //height of clut ClutDataSize = (int)(ClutSize - 12);//(NumOfColors * NumOfCluts*2); if (Assert(ClutDataSize == NumOfColors * NumOfCluts * 2 || ClutDataSize == NumOfColors * NumOfCluts) || Assert(PaletteX % 16 == 0) || Assert(PaletteY <= 511)) return; if (bpp == 4 && NumOfColors > 16) { // tim viewer was overriding the read number of colors per pixel to 16 // and this makes sense because you cannot read more than 16 colors with only a 4 bit value. // though this might break stuff. NumOfColors = 16; NumOfCluts = checked((ushort)(ClutDataSize / (NumOfColors * 2))); } Assert(bpp == 8 && NumOfColors <= 256 || bpp != 8); ClutData = br.ReadBytes(ClutDataSize); //br.BaseStream.Seek(start+clutSize, SeekOrigin.Begin); } //wmsetus uses 4BPP, but sets 256 colors, but actually is 16, but num of clut is 2* 256/16 WTF? ImageSize = br.ReadUInt32(); // image size + header in bytes ImageOrgX = br.ReadUInt16(); ImageOrgY = br.ReadUInt16(); Width = br.ReadUInt16(); Height = br.ReadUInt16(); ImageDataSize = (int)(ImageSize - 12);//(NumOfColors * NumOfCluts*2); Assert(ImageDataSize == Width * Height * 2); // Pixel data is stored in uint16 spots. So you use this to convert that to the // correct color / CLUT color key If 32 bit existed it'd be 1:2 24 bit is 1:1.5 16 bit // is 1:1 8 bit is 2:1 4 bit is 4:1 if (bpp == 4) Width = (ushort)(Width * 4); if (bpp == 8) Width = (ushort)(Width * 2); //if (_bpp == 16) // Width = Width; if (bpp == 24) { Width = (ushort)(Width / 1.5); // The below i'm unsure if any of this effects TIM files. So I commented them out. // http://www.elisanet.fi/6581/PSX/doc/Playstation_Hardware.pdf // http://www.elisanet.fi/6581/PSX/doc/psx.pdf //24 bit color mode has different restrictions. I guess you bypass the psx gpu to draw these //Assert(Width >= 8 && Height >= 2);// && // not sure if the multiple of 8 for width is right. } //Assert(Width <= 256 && Height <= 256 && Width > 0 && Height > 0); // sprite max size 256x256 and min size 1x1 //Assert(Width % 8 == 0 && Height % 8 == 0); // maybe texture must be multiple of 8. } #endregion Methods } #endregion Structs } }