SWFファイルから画像を抽出する

Flashのswfファイルから画像を抽出する…わけは艦これ諜報員用なのですが、完全に実装する前にメモ程度に残しておきます。いくつか、MFCのライブラリとかJava,PHPのライブラリはあるのですが、やっぱりC#のほうがいいですよね。

neue cc – C#でFlash Liteなswfをバイナリ編集して置換する
http://neue.cc/2013/01/10_393.html
debreuil/Swf2XNA
https://github.com/debreuil/Swf2XNA
SWFバイナリ編集のススメ第三回 (JPEG) | GREE Engineers’ Blog
http://labs.gree.jp/blog/2010/09/782/

github に Swf2XNA というプロジェクトがあります。neue さん言う通り、完全に使うには XNA のライブラリが必要なのですが、そこまで面倒ではなくて SwfFormat プロジェクトだけが必要です。

※一応、但し書きを書いておきますが、画像の著作権は艦これ自体に帰属するので、悪用しないように。

■手っ取り早く画像ファイルを抽出したい

用途としては、艦これでダウンロードしている *.swf ファイルから画像を抜き出したいだけです。画像の取り出し自体は HugFlash – 窓の杜ライブラリ  というツールがあるので、特に問題はありません。艦これ諜報員の榛名の画像は、これを使って取り出したものですが。が、どうせならば実行時に画像を取り出したいですよね。そうすると、秘書艦を変えたときに、同期して諜報員の画像も変えることがでいそうです。

さて、先の記事を読むと Flash の中身は Tag に分かれています。この中で画像に関係するところの

  • TagType.DefineBits
  • TagType.DefineBitsJPEG2
  • TagType.DefineBitsJPEG3

の3つを取り出せばよさそうです。実は、TagType.DefineBits のときの JPEGTables+DefineBits の組み合わせは真面目にやらないといけないのですが、幸いにして(?)、艦これで扱っている swf は JPEGTables の長さが 0 のために、そのまま DefineBits 内のバイナリデータが使えます。これでいいのかどうかは不明なのですが、まあ、用途に足りるので。

private void button1_Click(object sender, EventArgs e)
{
    var reader = new SwfReader(File.ReadAllBytes(@"d:work14.swf"));
    var swf = new SwfCompilationUnit(reader);
    var lst = swf.Tags.FindAll(x =>
        x.TagType == TagType.DefineBits ||
        x.TagType == TagType.DefineBitsJPEG3 ||
        x.TagType == TagType.DefineBitsJPEG2);
    int i = 1;
    foreach (var it in lst)
    {
        var tag = it as DefineBitsTag;
        var path = string.Format(@"d:work14-{0}.png",tag.CharacterId);
        Bitmap bmp = tag.ToBitmap();
        if (bmp != null)
        {
            bmp.Save(path, ImageFormat.Png);
        }
        i++;
    }
}

File.ReadAllBytes メソッドで byte[] として取り出しデータを SwfReader を喰わせた後、SwfCompilationUnit クラスでパースします。そのあと、Tags コレクションの中身を覗けばOKです。
DefineBitsTag クラスの ToBitmap メソッドは自作の拡張メソッドです。

public static class DefineBitsTagExtention
{
    public static Bitmap ToBitmap(this DefineBitsTag tag)
    {
        Bitmap png = null;
        if (tag.TagType == TagType.DefineBitsJPEG2 ||
            tag.TagType == TagType.DefineBitsJPEG3)
        {
            if (tag.HasAlphaData == false)
            {
                var mem = new MemoryStream(tag.JpegData);
                png = new Bitmap(mem);
                //mem.Close();
            }
            else
            {
                var mem = new MemoryStream(tag.JpegData);
                Bitmap bmp = new Bitmap(mem);
                png = new Bitmap(bmp.Width, bmp.Height);
                Graphics g = Graphics.FromImage(png);
                g.DrawImage(bmp, 0, 0);
                var alpha = SwfReader.Decompress(tag.CompressedAlphaData,
					(uint)(bmp.Width * bmp.Height));
                int j = 0;
                for (int y = 0; y < bmp.Height; y++)
                {
                    for (int x = 0; x < bmp.Width; x++)
                    {
                        int a = alpha[j];
                        var c = bmp.GetPixel(x, y);
                        var col = Color.FromArgb(a, c);
                        png.SetPixel(x, y, col);
                        j++;
                    }
                }
            }
        }
        else if (tag.TagType == TagType.DefineBits)
        {
            int size = tag.JpegData.Count() + 4;
            byte[] buf = new byte[size];

            for (int i = 0; i < tag.JpegData.Count(); i++)
            {
                buf[i + 2] = tag.JpegData[i];
            }
            buf[0] = 0xFF;
            buf[1] = 0xD8;
            buf[2+tag.JpegData.Count()] = 0xFF;
            buf[2+tag.JpegData.Count()+1] = 0xD9;
            var mem = new MemoryStream(buf);
            png = new Bitmap(mem);
            // mem.Close();
        }
        return png;
    }
}

DefineBitsJPEG2 と DefineBitsJPEG3 の場合は、画像ファイルそのもの(jpeg,png,gif)が JpegData 配列に入っているので、Bitmap に落とせます。アルファチャンネルがある場合は、HasAlphaData をチェックして透過データを追加してやります。いちいち GetPixel/SetPixel とやるとスピードが遅いので、適当に unsafe してバイナリを直接扱うとかすると良いでしょう。実際 SwfFormat の中ではそういうコードがあります。

JPEGTables + DefineBits の組み合わせのときは、本当はきちんとやらないと駄目なのですが、そもそもが JPEGTables のサイズが 0 なので、ここでは決め打ちで書いています。前後に SOI と EOI を追加してやれば JPENG として読み込みます。

こんな風に全ての画像ファイルを抽出できます…と思ったのですが、HugFlash の出力と見比べると、ひとつだけファイルが足りないんですよね。どうやら、TagType.DefineBitsLossless か TagType.DefineBitsLossless2 に入っている画像データを忘れていた模様。これはちょっと後で。

■秘書艦の切り替えメモ

こんな訳で、リアルタイムに画像抽出ができそうです。ちなみに SwfFormat を使うと「画像改変」もできるので、Faddler と組み合わせれば、あんな画像やこんな画像ですり替えることは可能ですね。ただし、Faddler 自体がプロキシなので差し替えは可能だし、しかし、304 でローカルキャッシュから持ってきている問題もあるので簡単には差し替えできないという要因もありますが。

ひとまず、13番と15番決め打ちで画像を取り出して、諜報員の画像を切りかえることは可能そうです。

カテゴリー: 開発, C# パーマリンク