From 08836a156d33c878a3583fc5b46543bd61ef6dc5 Mon Sep 17 00:00:00 2001 From: morkt Date: Wed, 9 Mar 2016 10:57:50 +0400 Subject: [PATCH] implemented SPC compressed textures. --- ArcFormats/ArcFormats.csproj | 2 + ArcFormats/Cri/ArcSPC.cs | 176 +++++++++++++++++++++++++++++ ArcFormats/Cri/ImageSPC.cs | 209 +++++++++++++++++++++++++++++++++++ ArcFormats/Cri/ImageXTX.cs | 2 +- 4 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 ArcFormats/Cri/ArcSPC.cs create mode 100644 ArcFormats/Cri/ImageSPC.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 1762a14c..adb204e3 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -68,8 +68,10 @@ + + diff --git a/ArcFormats/Cri/ArcSPC.cs b/ArcFormats/Cri/ArcSPC.cs new file mode 100644 index 00000000..4a05060f --- /dev/null +++ b/ArcFormats/Cri/ArcSPC.cs @@ -0,0 +1,176 @@ +//! \file ArcSPC.cs +//! \date Wed Mar 09 09:01:11 2016 +//! \brief CRI container for multiple textures. +// +// Copyright (C) 2016 by morkt +// +// 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.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using GameRes.Compression; + +namespace GameRes.Formats.Cri +{ + [Export(typeof(ArchiveFormat))] + public class SpcOpener : ArchiveFormat + { + public override string Tag { get { return "SPC/CRI"; } } + public override string Description { get { return "CRI MiddleWare texture container"; } } + public override uint Signature { get { return 0; } } + public override bool IsHierarchic { get { return true; } } + public override bool CanCreate { get { return false; } } + + public SpcOpener () + { + Extensions = new string[] { "spc" }; + } + + public override ArcFile TryOpen (ArcView file) + { + if (!file.Name.EndsWith (".spc", StringComparison.InvariantCultureIgnoreCase)) + return null; + uint unpacked_size = file.View.ReadUInt32 (0); + if (unpacked_size <= 0x20 || unpacked_size > 0x5000000) + return null; + + var backend = file.CreateStream(); + backend.Position = 4; + var lzss = new LzssStream (backend); + var input = new SeekableStream (lzss); + try + { + var base_name = Path.GetFileNameWithoutExtension (file.Name); + using (var spc = new XtxIndexBuilder (input, base_name)) + { + spc.ReadIndex (0); + if (spc.Dir.Count > 0) + return new SpcArchive (file, this, spc.Dir, input); + else + throw new InvalidFormatException(); + } + } + catch + { + input.Dispose(); + throw; + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + return new StreamRegion (((SpcArchive)arc).Source, entry.Offset, entry.Size, true); + } + } + + internal sealed class XtxIndexBuilder : IDisposable + { + BinaryReader m_input; + string m_base_name; + List m_dir = new List(); + int m_subdir_count = 0; + + public List Dir { get { return m_dir; } } + + public XtxIndexBuilder (Stream input, string base_name) + { + m_input = new ArcView.Reader (input); + m_base_name = base_name; + } + + public void ReadIndex (uint base_offset, string dir_name = "") + { + m_input.BaseStream.Position = base_offset; + uint first_offset = m_input.ReadUInt32(); + if (0 != (first_offset & 0xF)) + throw new InvalidFormatException(); + int count = (int)(first_offset / 0x10u); + m_input.BaseStream.Position = base_offset; + var subdir = new List (count); + for (int i = 0; i < count; ++i) + { + uint offset = m_input.ReadUInt32(); + uint size = m_input.ReadUInt32(); + if (offset < first_offset || size < 0x20) + throw new InvalidFormatException(); + m_input.BaseStream.Seek (8, SeekOrigin.Current); + var entry = new Entry { Offset = base_offset + offset, Size = size }; + subdir.Add (entry); + } + foreach (var entry in subdir) + { + m_input.BaseStream.Position = entry.Offset; + uint signature = m_input.ReadUInt32(); + if (0x787478 == signature) // 'xtx' + { + var file_name = string.Format ("{0}#{1:D4}.xtx", m_base_name, m_dir.Count); + entry.Name = Path.Combine (dir_name, file_name); + entry.Type = "image"; + m_dir.Add (entry); + } + else + { + var subdir_name = m_subdir_count++.ToString ("D4"); + ReadIndex ((uint)entry.Offset, Path.Combine (dir_name, subdir_name)); + } + } + } + + #region IDisposable Members + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_input.Dispose(); + _disposed = true; + } + } + #endregion + } + + internal class SpcArchive : ArcFile + { + public readonly Stream Source; + + public SpcArchive (ArcView arc, ArchiveFormat impl, ICollection dir, Stream input) + : base (arc, impl, dir) + { + Source = input; + } + + #region IDisposable Members + bool _spc_disposed = false; + protected override void Dispose (bool disposing) + { + if (_spc_disposed) + return; + if (disposing) + { + Source.Dispose(); + } + _spc_disposed = true; + base.Dispose (disposing); + } + #endregion + } +} diff --git a/ArcFormats/Cri/ImageSPC.cs b/ArcFormats/Cri/ImageSPC.cs new file mode 100644 index 00000000..b26f8f6e --- /dev/null +++ b/ArcFormats/Cri/ImageSPC.cs @@ -0,0 +1,209 @@ +//! \file ImageSPC.cs +//! \date Tue Mar 08 19:05:11 2016 +//! \brief CRI MiddleWare compressed multi-frame image. +// +// Copyright (C) 2016 by morkt +// +// 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 GameRes.Compression; + +namespace GameRes.Formats.Cri +{ + [Export(typeof(ImageFormat))] + public class SpcFormat : XtxFormat + { + public override string Tag { get { return "SPC"; } } + public override string Description { get { return "CRI MiddleWare compressed texture format"; } } + public override uint Signature { get { return 0; } } + + public SpcFormat () + { + Signatures = new uint[] { 0 }; + } + + public override ImageMetaData ReadMetaData (Stream stream) + { + uint unpacked_size = FormatCatalog.ReadSignature (stream); + if (unpacked_size <= 0x20 || unpacked_size > 0x5000000) // ~83MB + return null; + using (var lzss = new LzssStream (stream, LzssMode.Decompress, true)) + using (var input = new SeekableStream (lzss)) + return base.ReadMetaData (input); + } + + public override ImageData Read (Stream stream, ImageMetaData info) + { + stream.Position = 4; + using (var lzss = new LzssStream (stream, LzssMode.Decompress, true)) + using (var input = new SeekableStream (lzss)) + return base.Read (input, info); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("SpcFormat.Write not implemented"); + } + } + + public class SeekableStream : Stream + { + Stream m_source; + Stream m_buffer; + bool m_should_dispose; + bool m_source_depleted; + long m_read_pos; + + public SeekableStream (Stream input, bool leave_open = false) + { + m_source = input; + m_should_dispose = !leave_open; + m_read_pos = 0; + if (m_source.CanSeek) + { + m_buffer = m_source; + m_source_depleted = true; + } + else + { + m_buffer = new MemoryStream(); + m_source_depleted = false; + } + } + + #region IO.Stream Members + public override bool CanRead { get { return m_buffer.CanRead; } } + public override bool CanSeek { get { return true; } } + public override bool CanWrite { get { return false; } } + public override long Length + { + get + { + if (!m_source_depleted) + { + m_buffer.Seek (0, SeekOrigin.End); + m_source.CopyTo (m_buffer); + m_source_depleted = true; + } + return m_buffer.Length; + } + } + public override long Position + { + get { return m_read_pos; } + set { m_read_pos = value; } + } + + public override int Read (byte[] buffer, int offset, int count) + { + int total_read = 0; + if (m_source_depleted) + { + m_buffer.Position = m_read_pos; + total_read = m_buffer.Read (buffer, offset, count); + m_read_pos += total_read; + return total_read; + } + if (m_read_pos < m_buffer.Length) + { + int available = (int)Math.Min (m_buffer.Length-m_read_pos, count); + m_buffer.Position = m_read_pos; + total_read = m_buffer.Read (buffer, offset, available); + m_read_pos += total_read; + count -= total_read; + if (0 == count) + return total_read; + offset += total_read; + } + else + { + m_buffer.Seek (0, SeekOrigin.End); + while (m_read_pos > m_buffer.Length) + { + int b = m_source.ReadByte(); + if (-1 == b) + { + m_source_depleted = true; + return 0; + } + m_buffer.WriteByte ((byte)b); + } + } + int read = m_source.Read (buffer, offset, count); + m_read_pos += read; + m_buffer.Write (buffer, offset, read); + return total_read + read; + } + + public override void Flush() + { + } + + public override long Seek (long offset, SeekOrigin origin) + { + if (SeekOrigin.Begin == origin) + m_read_pos = offset; + else if (SeekOrigin.Current == origin) + m_read_pos += offset; + else + m_read_pos = Length + offset; + + return m_read_pos; + } + + public override void SetLength (long length) + { + throw new NotSupportedException ("SeekableStream.SetLength method is not supported"); + } + + public override void Write (byte[] buffer, int offset, int count) + { + throw new NotSupportedException ("SeekableStream.Write method is not supported"); + } + + public override void WriteByte (byte value) + { + throw new NotSupportedException ("SeekableStream.WriteByte method is not supported"); + } + #endregion + + #region IDisposable Members + bool _disposed = false; + protected override void Dispose (bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + if (m_should_dispose) + m_source.Dispose(); + if (m_buffer != m_source) + m_buffer.Dispose(); + } + _disposed = true; + base.Dispose (disposing); + } + #endregion + } +} diff --git a/ArcFormats/Cri/ImageXTX.cs b/ArcFormats/Cri/ImageXTX.cs index 07fb230e..a40848b7 100644 --- a/ArcFormats/Cri/ImageXTX.cs +++ b/ArcFormats/Cri/ImageXTX.cs @@ -59,7 +59,7 @@ namespace GameRes.Formats.Cri if (!Binary.AsciiEqual (header, 0, "xtx\0")) { var header_size = LittleEndian.ToUInt32 (header, 0); - if (header_size >= stream.Length) + if (header_size >= 0x1000) // XXX use some arbitrary "large" value to avoid call to Stream.Length return null; stream.Position = header_size; if (0x20 != stream.Read (header, 0, 0x20))