From a1a9a177031d811785cfe569dba181ab52ac8870 Mon Sep 17 00:00:00 2001 From: morkt Date: Fri, 15 Jan 2016 11:56:10 +0400 Subject: [PATCH] implemented encrypted AZ system archives and TYP1 images. --- ArcFormats/AZSys/ArcEncrypted.cs | 348 ++++++++++++++++++++++++ ArcFormats/AZSys/FastMersenneTwister.cs | 208 ++++++++++++++ ArcFormats/AZSys/ImageTYP1.cs | 183 +++++++++++++ ArcFormats/ArcFormats.csproj | 5 + supported.html | 5 +- 5 files changed, 747 insertions(+), 2 deletions(-) create mode 100644 ArcFormats/AZSys/ArcEncrypted.cs create mode 100644 ArcFormats/AZSys/FastMersenneTwister.cs create mode 100644 ArcFormats/AZSys/ImageTYP1.cs diff --git a/ArcFormats/AZSys/ArcEncrypted.cs b/ArcFormats/AZSys/ArcEncrypted.cs new file mode 100644 index 00000000..58fec596 --- /dev/null +++ b/ArcFormats/AZSys/ArcEncrypted.cs @@ -0,0 +1,348 @@ +//! \file ArcEncrypted.cs +//! \date Thu Jan 14 03:27:52 2016 +//! \brief Encrypted AZ system resource archives. +// +// 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 GameRes.Compression; +using GameRes.Utility; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; + +namespace GameRes.Formats.AZSys +{ + [Serializable] + public class EncryptionScheme + { + public readonly uint IndexKey; + public readonly uint? ContentKey; + + public static readonly uint[] DefaultSeed = { 0x2F4D7DFE, 0x47345292, 0x1BA5FE82, 0x7BC04525 }; + + public EncryptionScheme (uint ikey, uint ckey) + { + IndexKey = ikey; + ContentKey = ckey; + } + + public EncryptionScheme (uint[] iseed) + { + IndexKey = GenerateKey (iseed); + ContentKey = null; + } + + public EncryptionScheme (uint[] iseed, byte[] cseed) + { + IndexKey = GenerateKey (iseed); + ContentKey = GenerateContentKey (cseed); + } + + public static uint GenerateKey (uint[] seed) + { + if (null == seed) + throw new ArgumentNullException ("seed"); + if (seed.Length < 4) + throw new ArgumentException(); + byte[] seed_bytes = new byte[0x10]; + Buffer.BlockCopy (seed, 0, seed_bytes, 0, 0x10); + uint key = Crc32.UpdateCrc (~seed[0], seed_bytes, 0, seed_bytes.Length) + ^ Crc32.UpdateCrc (~(seed[1] & 0xFFFF), seed_bytes, 0, seed_bytes.Length) + ^ Crc32.UpdateCrc (~(seed[1] >> 16), seed_bytes, 0, seed_bytes.Length) + ^ Crc32.UpdateCrc (~seed[2], seed_bytes, 0, seed_bytes.Length) + ^ Crc32.UpdateCrc (~seed[3], seed_bytes, 0, seed_bytes.Length); + return seed[0] ^ ~key; + } + + public static uint GenerateContentKey (byte[] env_bytes) + { + if (null == env_bytes) + throw new ArgumentNullException ("env_bytes"); + if (env_bytes.Length < 0x10) + throw new ArgumentException(); + uint crc = Crc32.Compute (env_bytes, 0, 0x10); + var sfmt = new FastMersenneTwister (crc); + var seed = new uint[4]; + seed[0] = sfmt.GetRand32(); + seed[1] = sfmt.GetRand32() & 0xFFFF; + seed[1] |= sfmt.GetRand32() << 16; + seed[2] = sfmt.GetRand32(); + seed[3] = sfmt.GetRand32(); + return GenerateKey (seed); + } + } + + [Serializable] + public class AzScheme : ResourceScheme + { + public Dictionary KnownSchemes; + } + + internal class AzArchive : ArcFile + { + public readonly uint SysenvKey; + public readonly uint RegularKey; + + public AzArchive (ArcView arc, ArchiveFormat impl, ICollection dir, uint syskey, uint regkey) + : base (arc, impl, dir) + { + SysenvKey = syskey; + RegularKey = regkey; + } + } + + [Export(typeof(ArchiveFormat))] + public class ArcEncryptedOpener : ArchiveFormat + { + public override string Tag { get { return "ARC/AZ/encrypted"; } } + public override string Description { get { return "AZ system encrypted resource archive"; } } + public override uint Signature { get { return 0; } } + public override bool IsHierarchic { get { return false; } } + public override bool CanCreate { get { return false; } } + + public static Dictionary KnownSchemes = new Dictionary + { + { "Default", new EncryptionScheme (EncryptionScheme.DefaultSeed) }, + }; + + public override ResourceScheme Scheme + { + get { return new AzScheme { KnownSchemes = KnownSchemes }; } + set { KnownSchemes = ((AzScheme)value).KnownSchemes; } + } + + public ArcEncryptedOpener () + { + Extensions = new string[] { "arc" }; + Signatures = new uint[] { 0x53EA06EB, 0x74F98F2F }; + } + + EncryptionScheme CurrentScheme; + + public override ArcFile TryOpen (ArcView file) + { + byte[] header_encrypted = file.View.ReadBytes (0, 0x30); + if (header_encrypted.Length < 0x30) + return null; + byte[] header = new byte[header_encrypted.Length]; + if (CurrentScheme != null) + { + try + { + Buffer.BlockCopy (header_encrypted, 0, header, 0, header.Length); + Decrypt (header, 0, CurrentScheme.IndexKey); + if (Binary.AsciiEqual (header, 0, "ARC\0")) + { + var arc = ReadIndex (file, header, CurrentScheme); + if (null != arc) + return arc; + } + } + catch { /* ignore parse errors */ } + } + foreach (var scheme in KnownSchemes.Values) + { + Buffer.BlockCopy (header_encrypted, 0, header, 0, header.Length); + Decrypt (header, 0, scheme.IndexKey); + if (Binary.AsciiEqual (header, 0, "ARC\0")) + { + var arc = ReadIndex (file, header, scheme); + if (null != arc) + CurrentScheme = new EncryptionScheme (arc.SysenvKey, arc.RegularKey); + return arc; + } + } + return null; + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var azarc = arc as AzArchive; + if (null == azarc) + return base.OpenEntry (arc, entry); + var data = arc.File.View.ReadBytes (entry.Offset, entry.Size); + if (entry.Name.Equals ("sysenv.tbl", StringComparison.InvariantCultureIgnoreCase)) + { + Decrypt (data, entry.Offset, azarc.SysenvKey); + return UnpackData (data); + } + Decrypt (data, entry.Offset, azarc.RegularKey); + if (data.Length > 0x14 && Binary.AsciiEqual (data, 0, "ASB\0")) + { + return OpenAsb (data); + } + return new MemoryStream (data); + } + + Stream OpenAsb (byte[] data) + { + int packed_size = LittleEndian.ToInt32 (data, 4); + if (packed_size <= 4 || packed_size > data.Length-0x10) + return new MemoryStream (data); + + uint unpacked_size = LittleEndian.ToUInt32 (data, 8); + uint key = unpacked_size ^ 0x9E370001; + unsafe + { + fixed (byte* raw = &data[0x10]) + { + uint* data32 = (uint*)raw; + for (int i = packed_size/4; i > 0; --i) + *data32++ -= key; + } + } + var asb = UnpackData (data, 0x10); + var header = new byte[0x10]; + Buffer.BlockCopy (data, 0, header, 0, 0x10); + return new PrefixStream (header, asb); + } + + byte[] ReadSysenvSeed (ArcView file, Entry entry, uint key) + { + var data = file.View.ReadBytes (entry.Offset, entry.Size); + if (data.Length <= 4) + throw new InvalidFormatException ("Invalid sysenv.tbl size"); + Decrypt (data, entry.Offset, key); + uint adler32 = LittleEndian.ToUInt32 (data, 0); + if (adler32 != Adler32.Compute (data, 4, data.Length-4)) + throw new InvalidEncryptionScheme(); + using (var input = new MemoryStream (data, 4, data.Length-4)) + using (var sysenv_stream = new ZLibStream (input, CompressionMode.Decompress)) + { + var seed = new byte[0x10]; + if (0x10 != sysenv_stream.Read (seed, 0, 0x10)) + throw new InvalidFormatException ("Invalid sysenv.tbl size"); + return seed; + } + } + + Stream UnpackData (byte[] data, int index = 0) + { + int length = data.Length - index; + if (length <= 4) + return new MemoryStream (data, index, length); + uint adler32 = LittleEndian.ToUInt32 (data, index); + if (adler32 != Adler32.Compute (data, index+4, length-4)) + return new MemoryStream (data, index, length); + var input = new MemoryStream (data, index+4, length-4); + return new ZLibStream (input, CompressionMode.Decompress); + } + + AzArchive ReadIndex (ArcView file, byte[] header, EncryptionScheme scheme) + { + int ext_count = LittleEndian.ToInt32 (header, 4); + int count = LittleEndian.ToInt32 (header, 8); + uint index_length = LittleEndian.ToUInt32 (header, 12); + if (ext_count < 1 || ext_count > 8 || !IsSaneCount (count) || index_length >= file.MaxOffset) + return null; + var packed_index = file.View.ReadBytes (header.Length, index_length); + if (packed_index.Length != index_length) + return null; + Decrypt (packed_index, header.Length, scheme.IndexKey); + uint checksum = LittleEndian.ToUInt32 (packed_index, 0); + if (checksum != Adler32.Compute (packed_index, 4, packed_index.Length-4)) + { + if (checksum != Crc32.Compute (packed_index, 4, packed_index.Length-4)) + throw new InvalidFormatException ("Index checksum mismatch"); + } + uint base_offset = (uint)header.Length + index_length; + using (var input = new MemoryStream (packed_index, 4, packed_index.Length-4)) + using (var zstream = new ZLibStream (input, CompressionMode.Decompress)) + using (var index = new BinaryReader (zstream)) + { + var dir = new List (count); + var name_buffer = new byte[0x20]; + for (int i = 0; i < count; ++i) + { + uint offset = index.ReadUInt32(); + uint size = index.ReadUInt32(); + uint crc = index.ReadUInt32(); + index.ReadInt32(); + if (name_buffer.Length != index.Read (name_buffer, 0, name_buffer.Length)) + return null; + var name = Binary.GetCString (name_buffer, 0, 0x20); + if (0 == name.Length) + return null; + var entry = FormatCatalog.Instance.Create (name); + entry.Offset = base_offset + offset; + entry.Size = size; + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + } + uint content_key = GetContentKey (file, dir, scheme); + return new AzArchive (file, this, dir, scheme.IndexKey, content_key); + } + } + + static void Decrypt (byte[] data, long offset, uint key) + { + ulong hash = key * 0x9E370001ul; + if (0 != (offset & 0x3F)) + { + hash = Binary.RotL (hash, (int)offset); + } + for (uint i = 0; i < data.Length; ++i) + { + data[i] ^= (byte)hash; + hash = Binary.RotL (hash, 1); + } + } + + uint GetContentKey (ArcView file, List dir, EncryptionScheme scheme) + { + if (null != scheme.ContentKey) + return scheme.ContentKey.Value; + + if ("system.arc".Equals (Path.GetFileName (file.Name), StringComparison.InvariantCultureIgnoreCase)) + { + var sysenv = dir.FirstOrDefault (e => e.Name.Equals ("sysenv.tbl", StringComparison.InvariantCultureIgnoreCase)); + if (null != sysenv) + { + var seed = ReadSysenvSeed (file, sysenv, scheme.IndexKey); + return EncryptionScheme.GenerateContentKey (seed); + } + } + else + { + var system_arc = VFS.CombinePath (Path.GetDirectoryName (file.Name), "system.arc"); + using (var arc = VFS.OpenView (system_arc)) + { + var header = arc.View.ReadBytes (0, 0x30); + Decrypt (header, 0, scheme.IndexKey); + using (var arc_file = ReadIndex (arc, header, scheme)) + { + var sysenv = arc_file.Dir.FirstOrDefault (e => e.Name.Equals ("sysenv.tbl", StringComparison.InvariantCultureIgnoreCase)); + if (null != sysenv) + { + var seed = ReadSysenvSeed (arc, sysenv, scheme.IndexKey); + return EncryptionScheme.GenerateContentKey (seed); + } + } + } + } + return scheme.IndexKey; + } + } +} diff --git a/ArcFormats/AZSys/FastMersenneTwister.cs b/ArcFormats/AZSys/FastMersenneTwister.cs new file mode 100644 index 00000000..5778866e --- /dev/null +++ b/ArcFormats/AZSys/FastMersenneTwister.cs @@ -0,0 +1,208 @@ +/** + * @brief SIMD oriented Fast Mersenne Twister(SFMT) + * + * @author Mutsuo Saito (Hiroshima University) + * @author Makoto Matsumoto (Hiroshima University) + * + * C# port by morkt + * + * Copyright (C) 2006, 2007 Mutsuo Saito, Makoto Matsumoto and Hiroshima University. + * Copyright (C) 2012 Mutsuo Saito, Makoto Matsumoto, Hiroshima University and The University of Tokyo. + * Copyright (C) 2013 Mutsuo Saito, Makoto Matsumoto and Hiroshima University. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the names of Hiroshima University, The University of + * Tokyo nor the names of its contributors may be used to endorse + * or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +namespace GameRes.Utility +{ + public class FastMersenneTwister + { + const int MEXP = 19937; + const int N = MEXP / 128 + 1; + const int N32 = N * 4; + const int POS1 = 122; + const int SL1 = 18; + const int SL2 = 1; + const int SR1 = 11; + const int SR2 = 1; + const uint MSK1 = 0xdfffffefU; + const uint MSK2 = 0xddfecb7fU; + const uint MSK3 = 0xbffaffffU; + const uint MSK4 = 0xbffffff6U; + const uint PARITY1 = 0x00000001U; + const uint PARITY2 = 0x00000000U; + const uint PARITY3 = 0x00000000U; + const uint PARITY4 = 0x13c9e684U; + + uint[,] m_state = new uint[N,4]; + int m_idx; + + public FastMersenneTwister (uint seed) + { + SRand (seed); + } + + public void SRand (uint seed) + { + uint prev = m_state[0,0] = seed; + for (int i = 1; i < N32; i++) + { + int p = i >> 2; + int k = i & 3; + prev = (uint)(1812433253UL * (prev ^ (prev >> 30)) + (uint)i); + m_state[p,k] = prev; + } + m_idx = N32; + period_certification(); + } + + public uint GetRand32 () + { + if (m_idx >= N32) + { + sfmt_gen_rand_all(); + m_idx = 0; + } + uint r = m_state[m_idx >> 2, m_idx & 3]; + m_idx++; + return r; + } + + /// + /// This function fills the internal state array with pseudorandom integers. + /// + void sfmt_gen_rand_all () + { + int i; + int r1 = N - 2; + int r2 = N - 1; + for (i = 0; i < N - POS1; i++) + { + do_recursion (i, i, i + POS1, r1, r2); + r1 = r2; + r2 = i; + } + for (; i < N; i++) + { + do_recursion (i, i, i + POS1 - N, r1, r2); + r1 = r2; + r2 = i; + } + } + + /// + /// This function represents the recursion formula. + /// + void do_recursion (int r, int a, int b, int c, int d) + { + var x = new uint[4]; + var y = new uint[4]; + lshift128 (x, a, SL2); + rshift128 (y, c, SR2); + m_state[r,0] = m_state[a,0] ^ x[0] ^ ((m_state[b,0] >> SR1) & MSK1) + ^ y[0] ^ (m_state[d,0] << SL1); + m_state[r,1] = m_state[a,1] ^ x[1] ^ ((m_state[b,1] >> SR1) & MSK2) + ^ y[1] ^ (m_state[d,1] << SL1); + m_state[r,2] = m_state[a,2] ^ x[2] ^ ((m_state[b,2] >> SR1) & MSK3) + ^ y[2] ^ (m_state[d,2] << SL1); + m_state[r,3] = m_state[a,3] ^ x[3] ^ ((m_state[b,3] >> SR1) & MSK4) + ^ y[3] ^ (m_state[d,3] << SL1); + } + + static readonly uint[] s_parity = { PARITY1, PARITY2, PARITY3, PARITY4 }; + + /// + /// This function certificate the period of 2^{MEXP} + /// + void period_certification () + { + uint inner = 0; + int i; + + for (i = 0; i < 4; i++) + inner ^= m_state[0,i] & s_parity[i]; + for (i = 16; i > 0; i >>= 1) + inner ^= inner >> i; + inner &= 1; + /* check OK */ + if (inner == 1) + return; + + /* check NG, and modification */ + for (i = 0; i < 4; i++) + { + uint work = 1; + for (int j = 0; j < 32; j++) + { + if ((work & s_parity[i]) != 0) + { + m_state[0,i] ^= work; + return; + } + work = work << 1; + } + } + } + + /// + /// This function simulates SIMD 128-bit left shift. + /// The 128-bit integer referenced by idx is shifted by (shift * 8) bits. + /// This function simulates the LITTLE ENDIAN SIMD. + /// + /// the output of this function + /// index within state array of the 128-bit data to be shifted + /// the shift value + void lshift128 (uint[] result, int idx, int shift) + { + ulong th = ((ulong)m_state[idx,3] << 32) | ((ulong)m_state[idx,2]); + ulong tl = ((ulong)m_state[idx,1] << 32) | ((ulong)m_state[idx,0]); + + ulong oh = th << (shift * 8); + ulong ol = tl << (shift * 8); + oh |= tl >> (64 - shift * 8); + result[1] = (uint)(ol >> 32); + result[0] = (uint)ol; + result[3] = (uint)(oh >> 32); + result[2] = (uint)oh; + } + + void rshift128 (uint[] result, int idx, int shift) + { + ulong th = ((ulong)m_state[idx,3] << 32) | ((ulong)m_state[idx,2]); + ulong tl = ((ulong)m_state[idx,1] << 32) | ((ulong)m_state[idx,0]); + + ulong oh = th >> (shift * 8); + ulong ol = tl >> (shift * 8); + ol |= th << (64 - shift * 8); + result[1] = (uint)(ol >> 32); + result[0] = (uint)ol; + result[3] = (uint)(oh >> 32); + result[2] = (uint)oh; + } + } +} diff --git a/ArcFormats/AZSys/ImageTYP1.cs b/ArcFormats/AZSys/ImageTYP1.cs new file mode 100644 index 00000000..ad5b9dd2 --- /dev/null +++ b/ArcFormats/AZSys/ImageTYP1.cs @@ -0,0 +1,183 @@ +//! \file ImageTYP1.cs +//! \date Thu Jan 14 18:45:18 2016 +//! \brief AZ system image format implementation. +// +// 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.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GameRes.Compression; + +namespace GameRes.Formats.AZSys +{ + internal class Typ1MetaData : CpbMetaData + { + public bool HasPalette; + } + + [Export(typeof(ImageFormat))] + public class Typ1Format : ImageFormat + { + public override string Tag { get { return "CPB/TYP1"; } } + public override string Description { get { return "AZ system image format"; } } + public override uint Signature { get { return 0x31505954; } } // 'TYP1' + + public Typ1Format () + { + Extensions = new string[] { "cpb" }; + } + + public override ImageMetaData ReadMetaData (Stream stream) + { + stream.Position = 4; + int bpp = stream.ReadByte(); + bool has_palette = stream.ReadByte() != 0; + using (var input = new ArcView.Reader (stream)) + { + var info = new Typ1MetaData { BPP = bpp, HasPalette = has_palette }; + info.Width = input.ReadUInt16(); + info.Height = input.ReadUInt16(); + input.ReadInt32(); + info.Channel[0] = input.ReadUInt32(); + info.Channel[1] = input.ReadUInt32(); + info.Channel[2] = input.ReadUInt32(); + info.Channel[3] = input.ReadUInt32(); + return info; + } + } + + public override ImageData Read (Stream stream, ImageMetaData info) + { + var meta = (Typ1MetaData)info; + var reader = new Reader (stream, meta); + reader.Unpack(); + return ImageData.Create (meta, reader.Format, reader.Palette, reader.Data); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("Typ1Format.Write not implemented"); + } + + internal class Reader + { + int m_width; + int m_height; + int m_bpp; + int m_pixel_size; + Stream m_input; + byte[] m_output; + uint[] m_channel; + + public PixelFormat Format { get; private set; } + public BitmapPalette Palette { get; private set; } + public byte[] Data { get { return m_output; } } + + public Reader (Stream input, Typ1MetaData info) + { + m_width = (int)info.Width; + m_height = (int)info.Height; + m_bpp = info.BPP; + m_pixel_size = 8 == m_bpp ? 1 : 4; + m_channel = info.Channel; + m_output = new byte[m_width * m_height * m_pixel_size]; + if (8 == m_bpp) + Format = info.HasPalette ? PixelFormats.Indexed8 : PixelFormats.Gray8; + else if (24 == m_bpp) + Format = PixelFormats.Bgr32; + else if (32 == m_bpp) + Format = PixelFormats.Bgra32; + else + throw new InvalidFormatException ("Invalid CPB color depth"); + m_input = input; + if (info.HasPalette) + { + m_input.Position = 0x1E; + Palette = ReadPalette(); + } + } + + public BitmapPalette ReadPalette () + { + var palette_data = new byte[0x400]; + if (0x400 != m_input.Read (palette_data, 0, 0x400)) + throw new InvalidFormatException(); + var palette = new Color[0x100]; + for (int i = 0; i < palette.Length; ++i) + { + int src = i * 4; + byte b = palette_data[src++]; + byte g = palette_data[src++]; + byte r = palette_data[src++]; + palette[i] = Color.FromRgb (r, g, b); + } + return new BitmapPalette (palette); + } + + public void Unpack () + { + if (8 == m_bpp) + UnpackIndexed(); + else + UnpackRGB(); + } + + void UnpackIndexed () + { + if (null == Palette) + m_input.Position = 0x22; + else + m_input.Position = 0x422; + using (var input = new ZLibStream (m_input, CompressionMode.Decompress, true)) + input.Read (m_output, 0, m_output.Length); + } + + static byte[] StreamMap = new byte[] { 3, 2, 1, 0 }; + static byte[] ChannelMap = new byte[] { 3, 0, 1, 2 }; + + void UnpackRGB () + { + byte[] channel = new byte[m_width*m_height]; + long start_pos = 0x1E; + for (int i = 0; i < 4; ++i) + { + if (0 == m_channel[StreamMap[i]]) + continue; + m_input.Position = start_pos + 4; // skip crc32 + using (var input = new ZLibStream (m_input, CompressionMode.Decompress, true)) + { + int channel_size = input.Read (channel, 0, channel.Length); + int dst = ChannelMap[i]; + for (int j = 0; j < channel_size; ++j) + { + m_output[dst] = channel[j]; + dst += m_pixel_size; + } + } + start_pos += m_channel[StreamMap[i]]; + } + } + } + } +} diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 98c076a0..f1bdb0a7 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -66,6 +66,9 @@ + + + @@ -98,6 +101,8 @@ + + diff --git a/supported.html b/supported.html index 7a34a94b..095678be 100644 --- a/supported.html +++ b/supported.html @@ -303,10 +303,11 @@ Sakura Synchronicity
Shinsetsu Ryouki no Ori Dai 2 Shou
*.elgELGNo -*.arcARC\x1aNoAZ System +*.arcARC\x1a
ARCNoAZ System Triptych
+Tsurugi Otome Noah
-*.cpbCPB\x1aNo +*.cpbCPB\x1a
TYP1No *.mfg
*.mfm
*.mfsALPFNoSilky'sJokei Kazoku *MFG_
MFGA
MFGCNo *.pmp
*.pmw-YesScenePlayerNyuujoku Hitozuma Jogakuen