572 lines
20 KiB
C#
Raw Normal View History

2018-12-19 03:45:04 +04:00
//! \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<SwfChunk> SoundStream = new List<SwfChunk>();
}
[Export(typeof(ArchiveFormat))]
public class SwfOpener : ArchiveFormat
{
public override string Tag { get { return "SWF"; } }
2019-02-25 12:55:46 +04:00
public override string Description { get { return "Shockwave Flash presentation"; } }
2018-12-19 03:45:04 +04:00
public override uint Signature { get { return 0; } }
public override bool IsHierarchic { get { return false; } }
public override bool CanWrite { get { return false; } }
public SwfOpener ()
{
2019-02-25 12:55:46 +04:00
Signatures = new uint[] { 0x08535743, 0x08535746, 0 };
2018-12-19 03:45:04 +04:00
}
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);
2019-02-25 12:55:46 +04:00
case Types.DefineBitsJpeg2:
return new SwfJpeg2Decoder (swent.Chunk);
2018-12-19 03:45:04 +04:00
case Types.DefineBitsJpeg3:
return new SwfJpeg3Decoder (swent.Chunk);
2019-02-25 12:55:46 +04:00
case Types.DefineBitsJpeg:
return OpenBitsJpeg (swent.Chunk);
2018-12-19 03:45:04 +04:00
default:
return base.OpenImage (arc, entry);
}
}
2019-02-25 12:55:46 +04:00
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);
}
2018-12-19 03:45:04 +04:00
delegate Stream Extractor (SwfEntry entry);
static Dictionary<Types, Extractor> ExtractMap = new Dictionary<Types, Extractor> {
// { Types.DoAction, ExtractChunkContents },
{ Types.DefineBitsJpeg, ExtractChunkContents },
{ Types.DefineBitsLossless, ExtractChunk },
{ Types.DefineBitsLossless2, ExtractChunk },
{ Types.DefineSound, ExtractAudio },
{ Types.SoundStreamHead, ExtractSoundStream },
{ Types.SoundStreamHead2, ExtractSoundStream },
};
static Dictionary<Types, string> TypeMap = new Dictionary<Types, string> {
{ Types.DefineBitsJpeg, "image" },
2019-02-25 12:55:46 +04:00
{ Types.DefineBitsJpeg2, "image" },
2018-12-19 03:45:04 +04:00
{ Types.DefineBitsJpeg3, "image" },
{ Types.DefineBitsLossless, "image" },
{ Types.DefineBitsLossless2, "image" },
{ Types.DefineSound, "audio" },
{ Types.DoAction, "" },
2019-02-25 12:55:46 +04:00
{ Types.JpegTables, "JpegTables" },
/*
{ Types.DefineText, "Text" },
{ Types.DefineText2, "Text2" },
{ Types.DefineVideoStream, "VideoStream" },
{ Types.VideoFrame, "VideoFrame" },
*/
2018-12-19 03:45:04 +04:00
};
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,
2019-02-25 12:55:46 +04:00
JpegTables = 8,
DefineText = 11,
2018-12-19 03:45:04 +04:00
DoAction = 12,
DefineSound = 14,
SoundStreamHead = 18,
SoundStreamBlock = 19,
DefineBitsLossless = 20,
DefineBitsJpeg2 = 21,
2019-02-25 12:55:46 +04:00
DefineShape2 = 22,
DefineShape3 = 32,
DefineText2 = 33,
2018-12-19 03:45:04 +04:00
DefineBitsJpeg3 = 35,
DefineBitsLossless2 = 36,
DefineSprite = 39,
SoundStreamHead2 = 45,
ExportAssets = 56,
2019-02-25 12:55:46 +04:00
DefineVideoStream = 60,
2018-12-19 03:45:04 +04:00
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<byte>();
}
}
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<SwfChunk> m_chunks = new List<SwfChunk>();
public List<SwfChunk> 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];
2019-02-25 12:55:46 +04:00
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;
2018-12-19 03:45:04 +04:00
pixels[i+3] = a;
}
}
return ImageData.Create (Info, Format, palette, pixels);
}
}
}
2019-02-25 12:55:46 +04:00
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 ()
{
}
}
2018-12-19 03:45:04 +04:00
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 ()
{
}
}
}