//! \file ImagePB3.cs //! \date Wed Dec 02 13:55:45 2015 //! \brief Cmvs engine image format. // // Copyright (C) 2015 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.Utility; using System; using System.ComponentModel.Composition; using System.IO; using System.Windows.Media; namespace GameRes.Formats.Purple { internal class Pb3MetaData : ImageMetaData { public int Type; public int SubType; public int InputSize; } [Export(typeof(ImageFormat))] public class Pb3Format : ImageFormat { public override string Tag { get { return "PB3"; } } public override string Description { get { return "Purple Software image format"; } } public override uint Signature { get { return 0x42334250; } } // 'PB3B' public override ImageMetaData ReadMetaData (Stream stream) { stream.Position = 4; using (var reader = new ArcView.Reader (stream)) { int input_size = reader.ReadInt32(); stream.Position = 0x18; int t2 = reader.ReadInt32(); int t1 = reader.ReadUInt16(); uint width = reader.ReadUInt16(); uint height = reader.ReadUInt16(); int bpp = reader.ReadUInt16(); return new Pb3MetaData { Width = width, Height = height, BPP = bpp, Type = t1, SubType = t2, InputSize = input_size, }; } } public override ImageData Read (Stream stream, ImageMetaData info) { var reader = new Pb3Reader (stream, (Pb3MetaData)info); reader.Unpack(); return ImageData.Create (info, reader.Format, null, reader.Data); } public override void Write (Stream file, ImageData image) { throw new System.NotImplementedException ("Pb3Format.Write not implemented"); } } internal sealed class Pb3Reader { byte[] m_input; Pb3MetaData m_info; int m_channels; int m_stride; byte[] m_output; byte[] m_lzss_frame; public PixelFormat Format { get; private set; } public byte[] Data { get { return m_output; } } public Pb3Reader (Stream input, Pb3MetaData info) { if (info.Type == 1 && info.SubType != 0x10) throw new NotSupportedException(); m_info = info; m_input = new byte[m_info.InputSize]; if (m_input.Length != input.Read (m_input, 0, m_input.Length)) throw new EndOfStreamException(); m_channels = m_info.BPP / 8; m_stride = 4 * (int)m_info.Width; m_lzss_frame = new byte[0x800]; Format = m_channels < 4 ? PixelFormats.Bgr32 : PixelFormats.Bgra32; // output array created by unpack methods as needed. } public void Unpack () { switch (m_info.Type) { default: throw new InvalidEncryptionScheme(); case 1: UnpackV1(); break; case 5: UnpackV5(); break; case 8: case 6: UnpackV6(); break; case 2: case 3: case 4: case 7: throw new NotSupportedException(string.Format ("PB3 v{0} images not supported", m_info.Type)); // V3 is pain in the ass to implement, machine code is full of unrolled loops resulting in a // thousands lines of spaghetti code. } } void UnpackV1 () { int width = (int)m_info.Width; int height = (int)m_info.Height; m_output = new byte[m_stride * height]; int plane_size = width * height; byte[] plane = new byte[plane_size]; int data1 = LittleEndian.ToInt32 (m_input, 0x2C); int data2 = LittleEndian.ToInt32 (m_input, 0x30); for (int channel = 0; channel < m_channels; ++channel) { int channel_offset = 4 * m_channels; for (int i = 0; i < channel; ++i) channel_offset += LittleEndian.ToInt32 (m_input, data1 + 4*i); int v21 = data1 + channel_offset; int bit_src = v21 + 12 + LittleEndian.ToInt32 (m_input, v21) + LittleEndian.ToInt32 (m_input, v21+4); int channel_size = LittleEndian.ToInt32 (m_input, v21 + 8); channel_offset = 4 * m_channels; for (int i = 0; i < channel; ++i) channel_offset += LittleEndian.ToInt32 (m_input, data2 + 4*i); int data_src = data2 + channel_offset; for (int i = 0; i < 0x7DE; ++i) m_lzss_frame[i] = 0; LzssUnpack (bit_src, data_src, plane, channel_size); int x_blocks = width >> 4; if (0 != (width & 0xF)) ++x_blocks; int y_blocks = height >> 4; if (0 != (height & 0xF)) ++y_blocks; if (0 == y_blocks || 0 == x_blocks) continue; int plane_src = 0; bit_src = v21 + 12; int bit_mask = 128; data_src = bit_src + LittleEndian.ToInt32 (m_input, v21); int v68 = 16; for (int y = 0; y < y_blocks; ++y) { int row = 16 * y; int v66 = 16; int dst_origin = m_stride * row + channel; // within m_output for (int x = 0; x < x_blocks; ++x) { int dst = dst_origin; int block_width = v66 > width ? width - 16 * x : 16; int block_height = v68 > height ? height - row : 16; if (0 == bit_mask) { ++bit_src; bit_mask = 128; } if (0 != (bit_mask & m_input[bit_src])) { byte b = m_input[data_src++]; for (int j = 0; j < block_height; ++j) { int v49 = dst; for (int i = 0; i < block_width; ++i) { m_output[v49] = b; v49 += 4; } dst += m_stride; } } else { for (int j = 0; j < block_height; ++j) { int v49 = dst; for (int i = 0; i < block_width; ++i) { m_output[v49] = plane[plane_src++]; v49 += 4; } dst += m_stride; } } bit_mask >>= 1; v66 += 16; dst_origin += 64; } v68 += 16; } } } void UnpackV5 () { m_output = new byte[m_stride * (int)m_info.Height]; for (int i = 0; i < 4; ++i) { int bit_src = 0x54 + LittleEndian.ToInt32 (m_input, 8 * i + 0x34); int data_src = 0x54 + LittleEndian.ToInt32 (m_input, 8 * i + 0x38); for (int j = 0; j < 0x7DE; ++j) m_lzss_frame[j] = 0; int frame_offset = 0x7DE; byte accum = 0; int bit_mask = 128; int dst = i; while (dst < m_output.Length) { if (0 == bit_mask) { ++bit_src; bit_mask = 128; } if (0 != (bit_mask & m_input[bit_src])) { int v = LittleEndian.ToUInt16 (m_input, data_src); data_src += 2; int count = (v & 0x1F) + 3; int offset = v >> 5; for (int k = 0; k < count; ++k) { byte b = m_lzss_frame[(k + offset) & 0x7FF]; m_lzss_frame[frame_offset++] = b; accum += b; m_output[dst] = accum; dst += 4; frame_offset &= 0x7FF; } } else { byte b = m_input[data_src++]; m_lzss_frame[frame_offset++] = b; accum += b; m_output[dst] = accum; dst += 4; frame_offset &= 0x7FF; } bit_mask >>= 1; } } } static readonly byte[] NameKeyV6 = { 0xA6, 0x75, 0xF3, 0x9C, 0xC5, 0x69, 0x78, 0xA3, 0x3E, 0xA5, 0x4F, 0x79, 0x59, 0xFE, 0x3A, 0xC7, }; void UnpackV6 () { var name_bytes = new byte[0x20]; int name_offset = 0x34; int i; for (i = 0; i < 0x20; ++i) { name_bytes[i] = (byte)(m_input[name_offset+i] ^ NameKeyV6[i & 0xF]); if (0 == name_bytes[i]) break; } m_output = LoadBaseImage (Encodings.cp932.GetString (name_bytes, 0, i) + ".pb3"); BlendInput(); } byte[] LoadBaseImage (string name) { // judging by the code, files with "pb3" extension could as well contain PNG or BMP images, // so we couldn't just shortcut to another instance of Pb3Reader here. var path = VFS.GetDirectoryName (m_info.FileName); name = VFS.CombinePath (path, name); if (name.Equals (m_info.FileName, StringComparison.InvariantCultureIgnoreCase)) throw new InvalidFormatException(); // two files referencing each other still could create infinite recursion using (var base_file = VFS.OpenSeekableStream (name)) { var image_data = ImageFormat.Read (name, base_file); int stride = image_data.Bitmap.PixelWidth * 4; var pixels = new byte[stride * image_data.Bitmap.PixelHeight]; image_data.Bitmap.CopyPixels (pixels, stride, 0); return pixels; } } void LzssUnpack (int bit_src, int data_src, byte[] output, int output_size) { int dst = 0; int bit_mask = 0x80; int frame_offset = 0x7DE; while (dst < output_size) { if (0 == bit_mask) { bit_mask = 0x80; ++bit_src; } if (0 != (bit_mask & m_input[bit_src])) { int v = LittleEndian.ToUInt16 (m_input, data_src); data_src += 2; int count = (v & 0x1F) + 3; int offset = v >> 5; for (int i = 0; i < count; ++i) { byte b = m_lzss_frame[(i + offset) & 0x7FF]; output[dst++] = b; m_lzss_frame[frame_offset++] = b; frame_offset &= 0x7FF; } } else { byte b = m_input[data_src++]; output[dst++] = b; m_lzss_frame[frame_offset++] = b; frame_offset &= 0x7FF; } bit_mask >>= 1; } } void BlendInput () { int bit_src = 0x20 + LittleEndian.ToInt32 (m_input, 0xC); int data_src = bit_src + LittleEndian.ToInt32 (m_input, 0x2C); int overlay_size = LittleEndian.ToInt32 (m_input, 0x18); var overlay = new byte[overlay_size]; LzssUnpack (bit_src, data_src, overlay, overlay_size); int width = (int)m_info.Width; int height = (int)m_info.Height; bit_src = 8; // within overlay data_src = 8 + LittleEndian.ToInt32 (overlay, 0); // within overlay int bit_mask = 0x80; int x_blocks = width >> 3; if (0 != (width & 7)) ++x_blocks; int y_blocks = height >> 3; if (0 != (height & 7)) ++y_blocks; if (0 == x_blocks) return; int h = 0; int dst_origin = 0; while (y_blocks > 0) { int w = 0; for (int x = 0; x < x_blocks; ++x) { if (0 == bit_mask) { ++bit_src; bit_mask = 0x80; } if (0 == (bit_mask & overlay[bit_src])) { int dst = 8 * (dst_origin + 4 * x); // within m_output int x_count = Math.Min (8, width - w); int y_count = Math.Min (8, height - h); for (int v30 = y_count; v30 > 0; --v30) { int count = 4 * x_count; Buffer.BlockCopy (overlay, data_src, m_output, dst, count); data_src += count; dst += m_stride; } } bit_mask >>= 1; w += 8; } dst_origin += m_stride; h += 8; --y_blocks; } } } }