//! \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 "Shockwave 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, 0x08535746, 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.DefineBitsJpeg2: return new SwfJpeg2Decoder (swent.Chunk); case Types.DefineBitsJpeg3: return new SwfJpeg3Decoder (swent.Chunk); case Types.DefineBitsJpeg: return OpenBitsJpeg (swent.Chunk); default: return base.OpenImage (arc, entry); } } IImageDecoder OpenBitsJpeg (SwfChunk chunk) { int jpeg_pos = 0; for (int i = 0; i < chunk.Data.Length - 2; ++i) { if (chunk.Data[i] == 0xFF && chunk.Data[i+1] == 0xD8) { jpeg_pos = i; break; } } var input = new BinMemoryStream (chunk.Data, jpeg_pos, chunk.Data.Length - jpeg_pos); return ImageFormatDecoder.Create (input); } 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, "image" }, { Types.DefineBitsJpeg3, "image" }, { Types.DefineBitsLossless, "image" }, { Types.DefineBitsLossless2, "image" }, { Types.DefineSound, "audio" }, { Types.DoAction, "" }, { Types.JpegTables, "JpegTables" }, /* { Types.DefineText, "Text" }, { Types.DefineText2, "Text2" }, { Types.DefineVideoStream, "VideoStream" }, { Types.VideoFrame, "VideoFrame" }, */ }; 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, JpegTables = 8, DefineText = 11, DoAction = 12, DefineSound = 14, SoundStreamHead = 18, SoundStreamBlock = 19, DefineBitsLossless = 20, DefineBitsJpeg2 = 21, DefineShape2 = 22, DefineShape3 = 32, DefineText2 = 33, DefineBitsJpeg3 = 35, DefineBitsLossless2 = 36, DefineSprite = 39, SoundStreamHead2 = 45, ExportAssets = 56, DefineVideoStream = 60, 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.Width = GetSignedBits (rsize) - m_dim.X; m_dim.Y = GetSignedBits (rsize); 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]; byte r = pixels[i+1]; byte g = pixels[i+2]; byte b = pixels[i+3]; pixels[i] = b; pixels[i+1] = g; pixels[i+2] = r; pixels[i+3] = a; } } return ImageData.Create (Info, Format, palette, pixels); } } } internal sealed class SwfJpeg2Decoder : IImageDecoder { byte[] m_input; ImageData m_image; public Stream Source { get { return Stream.Null; } } public ImageFormat SourceFormat { get { return null; } } public ImageMetaData Info { get; private set; } public ImageData Image { get { return m_image ?? (m_image = Unpack()); } } public SwfJpeg2Decoder (SwfChunk chunk) { m_input = chunk.Data; } ImageData Unpack () { int jpeg_pos = FindJpegSignature(); if (jpeg_pos < 0) throw new InvalidFormatException(); using (var jpeg = new BinMemoryStream (m_input, jpeg_pos, m_input.Length-jpeg_pos)) { var decoder = new JpegBitmapDecoder (jpeg, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); var frame = decoder.Frames[0]; Info = new ImageMetaData { Width = (uint)frame.PixelWidth, Height = (uint)frame.PixelHeight, BPP = frame.Format.BitsPerPixel, }; return new ImageData (frame, Info); } } int FindJpegSignature () { int jpeg_pos = 2; while (jpeg_pos < m_input.Length-4) { if (m_input[jpeg_pos] != 0xFF) jpeg_pos++; else if (m_input[jpeg_pos+1] == 0xD8) return jpeg_pos; else if (m_input[jpeg_pos+1] != 0xD9) jpeg_pos++; else if (m_input[jpeg_pos+2] != 0xFF) jpeg_pos += 3; else if (m_input[jpeg_pos+3] != 0xD8) jpeg_pos += 2; else return jpeg_pos+4; } return -1; } public void Dispose () { } } 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 () { } } }