diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 409d9185..9b59aabe 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -221,6 +221,7 @@ + diff --git a/ArcFormats/Macromedia/ArcSWF.cs b/ArcFormats/Macromedia/ArcSWF.cs new file mode 100644 index 00000000..c889dabc --- /dev/null +++ b/ArcFormats/Macromedia/ArcSWF.cs @@ -0,0 +1,474 @@ +//! \file ArcSWF.cs +//! \date 2018 Sep 16 +//! \brief Shockwave Flash presentation. +// +// Copyright (C) 2018 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 System.Linq; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GameRes.Compression; + +namespace GameRes.Formats.Macromedia +{ + internal class SwfEntry : Entry + { + public SwfChunk Chunk; + } + + internal class SwfSoundEntry : SwfEntry + { + public readonly List SoundStream = new List(); + } + + [Export(typeof(ArchiveFormat))] + public class SwfOpener : ArchiveFormat + { + public override string Tag { get { return "SWF"; } } + public override string Description { get { return "Shockeave Flash presentation"; } } + public override uint Signature { get { return 0; } } + public override bool IsHierarchic { get { return false; } } + public override bool CanWrite { get { return false; } } + + public SwfOpener () + { + Signatures = new uint[] { 0x08535743, 0 }; + } + + public override ArcFile TryOpen (ArcView file) + { + if (!file.View.AsciiEqual (0, "CWS") && + !file.View.AsciiEqual (0, "FWS")) + return null; + bool is_compressed = file.View.ReadByte (0) == 'C'; + int version = file.View.ReadByte (3); + using (var reader = new SwfReader (file.CreateStream(), version, is_compressed)) + { + var chunks = reader.Parse(); + var base_name = Path.GetFileNameWithoutExtension (file.Name); + var dir = chunks.Where (t => t.Length > 2 && TypeMap.ContainsKey (t.Type)) + .Select (t => new SwfEntry { + Name = string.Format ("{0}#{1:D5}", base_name, t.Id), + Type = GetTypeFromId (t.Type), + Chunk = t, + Offset = 0, + Size = (uint)t.Length + } as Entry).ToList(); + SwfSoundEntry current_stream = null; + foreach (var chunk in chunks.Where (t => IsSoundStream (t))) + { + switch (chunk.Type) + { + case Types.SoundStreamHead: + case Types.SoundStreamHead2: + if ((chunk.Data[1] & 0x30) != 0x20) // not mp3 stream + { + current_stream = null; + continue; + } + current_stream = new SwfSoundEntry { + Name = string.Format ("{0}#{1:D5}", base_name, chunk.Id), + Type = "audio", + Chunk = chunk, + Offset = 0, + }; + dir.Add (current_stream); + break; + + case Types.SoundStreamBlock: + if (current_stream != null) + { + current_stream.Size += (uint)(chunk.Data.Length - 4); + current_stream.SoundStream.Add (chunk); + } + break; + } + } + return new ArcFile (file, this, dir); + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var swent = (SwfEntry)entry; + Extractor extract; + if (!ExtractMap.TryGetValue (swent.Chunk.Type, out extract)) + extract = ExtractChunk; + return extract (swent); + } + + static string GetTypeFromId (Types type_id) + { + string type; + if (TypeMap.TryGetValue (type_id, out type)) + return type; + return type_id.ToString(); + } + + static Stream ExtractChunk (SwfEntry entry) + { + return new BinMemoryStream (entry.Chunk.Data); + } + + static Stream ExtractChunkContents (SwfEntry entry) + { + var source = entry.Chunk; + return new BinMemoryStream (source.Data, 2, source.Length-2); + } + + static Stream ExtractSoundStream (SwfEntry entry) + { + var swe = (SwfSoundEntry)entry; + var output = new MemoryStream ((int)swe.Size); + foreach (var chunk in swe.SoundStream) + output.Write (chunk.Data, 4, chunk.Data.Length-4); + output.Position = 0; + return output; + } + + static Stream ExtractAudio (SwfEntry entry) + { + var chunk = entry.Chunk; + int flags = chunk.Data[2]; + int format = flags >> 4; + if (2 == format) + return new BinMemoryStream (chunk.Data, 9, chunk.Length-9); + int sample_rate = (flags >> 2) & 3; + int bits_per_sample = (flags & 2) != 0 ? 16 : 8; + int channels = (flags & 1) + 1; + + return new BinMemoryStream (chunk.Data, 2, chunk.Length-2); + } + + public override IImageDecoder OpenImage (ArcFile arc, Entry entry) + { + var swent = (SwfEntry)entry; + switch (swent.Chunk.Type) + { + case Types.DefineBitsLossless: + case Types.DefineBitsLossless2: + return new LosslessImageDecoder (swent.Chunk); + + case Types.DefineBitsJpeg3: + return new SwfJpeg3Decoder (swent.Chunk); + + default: + return base.OpenImage (arc, entry); + } + } + + delegate Stream Extractor (SwfEntry entry); + + static Dictionary ExtractMap = new Dictionary { +// { Types.DoAction, ExtractChunkContents }, + { Types.DefineBitsJpeg, ExtractChunkContents }, + { Types.DefineBitsLossless, ExtractChunk }, + { Types.DefineBitsLossless2, ExtractChunk }, + { Types.DefineSound, ExtractAudio }, + { Types.SoundStreamHead, ExtractSoundStream }, + { Types.SoundStreamHead2, ExtractSoundStream }, + }; + + static Dictionary TypeMap = new Dictionary { + { Types.DefineBitsJpeg, "image" }, + { Types.DefineBitsJpeg2, "DefineBitsJpeg2" }, + { Types.DefineBitsJpeg3, "image" }, + { Types.DefineBitsLossless, "image" }, + { Types.DefineBitsLossless2, "image" }, + { Types.DefineSound, "audio" }, + { Types.DoAction, "" }, + }; + + internal static bool IsSoundStream (SwfChunk chunk) + { + return chunk.Type == Types.SoundStreamHead + || chunk.Type == Types.SoundStreamHead2 + || chunk.Type == Types.SoundStreamBlock; + } + } + + internal enum Types : short + { + End = 0, + ShowFrame = 1, + DefineShape = 2, + DefineBitsJpeg = 6, + DoAction = 12, + DefineSound = 14, + SoundStreamHead = 18, + SoundStreamBlock = 19, + DefineBitsLossless = 20, + DefineBitsJpeg2 = 21, + DefineBitsJpeg3 = 35, + DefineBitsLossless2 = 36, + DefineSprite = 39, + SoundStreamHead2 = 45, + ExportAssets = 56, + VideoFrame = 61, + FileAttributes = 69, + Font3 = 75, + DefineBinary = 87, + }; + + internal class SwfChunk + { + public Types Type; + public byte[] Data; + + public int Length { get { return Data.Length; } } + public int Id { get { return Data.Length > 2 ? Data.ToUInt16 (0) : -1; } } + + public SwfChunk (Types id, int length) + { + Type = id; + Data = length > 0 ? new byte[length] : Array.Empty(); + } + } + + internal sealed class SwfReader : IDisposable + { + IBinaryStream m_input; + MsbBitStream m_bits; + int m_version; + + Int32Rect m_dim; + + public SwfReader (IBinaryStream input, int version, bool is_compressed) + { + m_input = input; + m_version = version; + m_input.Position = 8; + if (is_compressed) + { + var zstream = new ZLibStream (input.AsStream, CompressionMode.Decompress); + m_input = new BinaryStream (zstream, m_input.Name); + } + m_bits = new MsbBitStream (m_input.AsStream, true); + } + + int m_frame_rate; + int m_frame_count; + + List m_chunks = new List(); + + public List Parse () + { + ReadDimensions(); + m_bits.Reset(); + m_frame_rate = m_input.ReadUInt16(); + m_frame_count = m_input.ReadUInt16(); + for (;;) + { + var chunk = ReadChunk(); + if (null == chunk) + break; + m_chunks.Add (chunk); + } + return m_chunks; + } + + void ReadDimensions () + { + int rsize = m_bits.GetBits (5); + m_dim.X = GetSignedBits (rsize); + m_dim.Y = GetSignedBits (rsize); + m_dim.Width = GetSignedBits (rsize) - m_dim.X; + m_dim.Height = GetSignedBits (rsize) - m_dim.Y; + } + + byte[] m_buffer = new byte[4]; + + SwfChunk ReadChunk () + { + if (m_input.Read (m_buffer, 0, 2) != 2) + return null; + int length = m_buffer.ToUInt16 (0); + Types id = (Types)(length >> 6); + length &= 0x3F; + if (0x3F == length) + length = m_input.ReadInt32(); + if (Types.DefineSprite == id) + length = 4; + var chunk = new SwfChunk (id, length); + if (length > 0) + { + if (m_input.Read (chunk.Data, 0, length) < length) + return null; + } + return chunk; + } + + int GetSignedBits (int count) + { + int v = m_bits.GetBits (count); + if ((v >> (count - 1)) != 0) + v |= -1 << count; + return v; + } + + #region IDisposable Members + bool m_disposed = false; + public void Dispose () + { + if (!m_disposed) + { + m_input.Dispose(); + m_bits.Dispose(); + m_disposed = true; + } + } + #endregion + } + + internal sealed class LosslessImageDecoder : BinaryImageDecoder + { + Types m_type; + int m_colors; + int m_data_pos; + + public PixelFormat Format { get; private set; } + private bool HasAlpha { get { return m_type == Types.DefineBitsLossless2; } } + + public LosslessImageDecoder (SwfChunk chunk) : base (new BinMemoryStream (chunk.Data)) + { + m_type = chunk.Type; + byte format = chunk.Data[2]; + int bpp; + switch (format) + { + case 3: + bpp = 8; Format = PixelFormats.Indexed8; + break; + case 4: + bpp = 16; Format = PixelFormats.Bgr565; + break; + case 5: + bpp = 32; + Format = HasAlpha ? PixelFormats.Bgra32 : PixelFormats.Bgr32; + break; + default: throw new InvalidFormatException(); + } + uint width = chunk.Data.ToUInt16 (3); + uint height = chunk.Data.ToUInt16 (5); + m_colors = 0; + m_data_pos = 7; + if (3 == format) + m_colors = chunk.Data[m_data_pos++] + 1; + Info = new ImageMetaData { + Width = width, Height = height, BPP = bpp + }; + } + + protected override ImageData GetImageData () + { + m_input.Position = m_data_pos; + using (var input = new ZLibStream (m_input.AsStream, CompressionMode.Decompress, true)) + { + BitmapPalette palette = null; + if (8 == Info.BPP) + { + var pal_format = HasAlpha ? PaletteFormat.RgbA : PaletteFormat.RgbX; + palette = ImageFormat.ReadPalette (input, m_colors, pal_format); + } + var pixels = new byte[(int)Info.Width * (int)Info.Height * (Info.BPP / 8)]; + input.Read (pixels, 0, pixels.Length); + if (32 == Info.BPP) + { + for (int i = 0; i < pixels.Length; i += 4) + { + byte a = pixels[i]; + pixels[i] = pixels[i+1]; + pixels[i+1] = pixels[i+2]; + pixels[i+2] = pixels[i+3]; + pixels[i+3] = a; + } + } + return ImageData.Create (Info, Format, palette, pixels); + } + } + } + + internal sealed class SwfJpeg3Decoder : IImageDecoder + { + byte[] m_input; + ImageData m_image; + int m_jpeg_length; + + public Stream Source { get { return Stream.Null; } } + public ImageFormat SourceFormat { get { return null; } } + public ImageMetaData Info { get; private set; } + public PixelFormat Format { get; private set; } + public ImageData Image { get { return m_image ?? (m_image = Unpack()); } } + + public SwfJpeg3Decoder (SwfChunk chunk) + { + m_input = chunk.Data; + m_jpeg_length = m_input.ToInt32 (2); + } + + ImageData Unpack () + { + BitmapSource image; + using (var jpeg = new BinMemoryStream (m_input, 6, m_jpeg_length)) + { + var decoder = new JpegBitmapDecoder (jpeg, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + image = decoder.Frames[0]; + } + Info = new ImageMetaData { + Width = (uint)image.PixelWidth, + Height = (uint)image.PixelHeight, + BPP = image.Format.BitsPerPixel, + }; + byte[] alpha = new byte[image.PixelWidth * image.PixelHeight]; + using (var input = new BinMemoryStream (m_input, 6 + m_jpeg_length, m_input.Length - (6+m_jpeg_length))) + using (var alpha_data = new ZLibStream (input, CompressionMode.Decompress)) + { + alpha_data.Read (alpha, 0, alpha.Length); + } + if (image.Format.BitsPerPixel != 32) + image = new FormatConvertedBitmap (image, PixelFormats.Bgr32, null, 0); + int stride = image.PixelWidth * 4; + var pixels = new byte[stride * image.PixelHeight]; + image.CopyPixels (pixels, stride, 0); + ApplyAlpha (pixels, alpha); + return ImageData.Create (Info, PixelFormats.Bgra32, null, pixels, stride); + } + + void ApplyAlpha (byte[] pixels, byte[] alpha) + { + int src = 0; + for (int dst = 3; dst < pixels.Length; dst += 4) + { + pixels[dst] = alpha[src++]; + } + } + + public void Dispose () + { + } + } +}