LoginSignup
2
3

More than 5 years have passed since last update.

SWF形式のヘッダを読んでみる

Last updated at Posted at 2018-09-02

今回は、2020年に終了が確定して居るAdobe Flash Plyerで使われているSWFファイルを読んでみます。10年ぐらい前に流行ったFlashアニメーションをレスキューしたいのが動機ですけどね(ただし、やりたいと出来るは別の話。)

SWFの仕様書を読むとオープンソースが作れないライセンスの問題は既に解消されている様なので、とりあえずタグだけ読んでみました。

用意するモノ

なに英語だと(苦手)。日本語に訳したヤツの方が意味不明な事が多いから気にしない。

githubに置こうかと思いましたが、少しデバッグしたい(小数点周りが全くデバッグ出来てない)ので全コードは現状のせません(のせられません)。

仕様書は、かなり丁寧に書いてありますが、Action Scriptの仕様書などは別にあるので、この仕様書だけではflash playerのcloneは作れません。そして、タグを読み取るだけなら最初のデータ格納方式からヘッダの部分(Chapter 1と2)と後ろの逆引きインデックス(Appendix B: Reverse index of tag values)だけ読めば十分です。

タグを読み取る時に使うデータ方式は、UINT8、UINT16、UINT32、RECT、UB[nbit]、SB[bit](RECTのSBはUBで読んでも問題起きるデータはあまりないかな)ぐらいですか。固定小数点や浮動小数点などは使いません(インプリメントはしたもののデータが無いのでデバッグできない。)

リハビリなので、あまり使ったことの無いPythonで書いてみました。

Python 2.7でつまったところ

バイナリデータをreadしたのに、Stringsで帰ってくるのはなぜですか?VB5かよ。
こんな感じに書いたけど綺麗にならないかな(主に速度的に)、エンディアン変換(swfはリトルエンディアンです。)も同じコードが使い回せるからいいか。

import struct

class ByteReader:

    def __init__ (self,path):
        self.f = open (path,"rb") 
        self.bitoffset = 0

    def __del__(self):
        self.f.close
    def getByte(self):
        byte = struct.unpack('<B', self.f.read(1))[0]
        return byte

    def getUINT16(self):
        num =   struct.unpack('<H',self.f.read(2))[0]
        return num

    def getSINT16(self):
        num=    struct.unpack('<h',self.f.read(2))[0]
        return num

    def getUINT32(self):
        num =   struct.unpack('<L',self.f.read(4))[0]
        return num

    def getSINT32(self):
        num =   struct.unpack('<l',self.f.read(4))[0]
        return num

    def getUINT64(self):
        num =   struct.unpack('<Q',self.f.read(8))[0]
        return num

    def getSINT64(self):
        num =   struct.unpack('<q',self.f.read(8))[0]
        return num

注意すべき点

解説ページが幾つもあるのでその辺を見てください。

この辺とか
http://labs.gree.jp/blog/2010/08/631/

UB[nbit]方式

データを圧縮する為によくやるヤツです。例えば0-3000までの数字しか使わなければ12bitあれば十分です。なのでデータを12bitにして書き込みます(12bitで0-4095まで表現出来ます)。その変わりデータがbyte境界をまたぎます。これを読み取るコードが意外と面倒です(圧縮やってるとよく出てくるヤツ)
これはbit数が最初に指定されているからまだ楽ですね。最悪なのは読んでみないとbit数が分からないヤツです。

後から追加されたデータ形式にEncodedU32と言う1-5byteを使う形式もありますが、今回は使いません。(UTF-8は、EncodedU64ですけど)

 UI[Nbits]を読む部分はこうしておきました。Byteに変換した時点で既にオーバーヘッドが大きいので再帰で関数呼ぶことにしました(swfぐらいであればオーバーヘッドが気になる可能性は少ないですが動画圧縮だとこのオーバーヘッドがパフォーマンスに影響します。) [Nbit]で格納されているデータ群は、それを読み終えたら残りのbitを捨てる事になってるので、bitoffsetをclearしないと行けません。

今回この形式で読むのはRECTだけです。

    def getBits(self,bits):
        if (bits <= 0):
            return 0
        num = 0;

        if (self.bitoffset <= 0)  :
            self.byte = self.getByte()
            self.bitoffset = 8

        byte = self.byte
        if (bits <= self.bitoffset):
            mask = (1 << bits) -1
            self.bitoffset -= bits
            num = byte >> self.bitoffset
            return num & mask 

        mask = (1 << (self.bitoffset)) -1
        num = (byte  & mask)
        nextbits = bits - self.bitoffset
        self.bitoffset = 0
        return (num << nextbits) | self.getBits(nextbits) 

    def bitclear(self):
        self.bitoffset = 0

ヘッダが、CWSで始まる

file lengthより後のデータはzlibに読ませると書いてあります。結論から言えば問答無用でzlibにぶちこんで問題無さそうです。

“C” indicates a zlib compressed SWF (SWF 6 and later only)

import zlib
import io
    def setCompressed(self):
        buf = zlib.decompress(self.f.read());
        self.f.close
        self.f = io.BytesIO(buf) 

ヘッダが、ZWS始まる

LZMAで圧縮してあると書いてありますが、サンプルが見つからないので今回は無視します。

“Z” indicates a LZMA compressed SWF (SWF 13 and later only)

import lzma で出来そうです。

インプリメント

ヘッダの読み出し

8byteまでは普通に読み出します。しかしCWSで始まるときは8byte以降がZLIBで圧縮されているので、ZLIBに解凍させます

if len(sys.argv) > 1:
    path = sys.argv[1]
else:
    print("Usage: swfread.py [filename]")
    sys.exit(1)

reader = ByteReader(path)

#CWS or FWS
header =  bytearray()
header.append( reader.getByte())
header.append( reader.getByte())
header.append( reader.getByte())
header = header.decode('ascii','ignore')
Logger("Signature:" + header)


#SWF version
version = reader.getByte()
Logger("Version:" +str(version))

#File length
filelength = reader.getUINT32()
Logger("FIlelegth:" + str(filelength))

if (header == 'FWS') : 
    Logger('Uncompressed data')
elif (header == 'CWS') : 
    Logger('Compressed data')
    reader.setCompressed() #If swf file is compressed, decode ZLIB
else:
    Logger('unknown')
    sys.exit(-1)

#Frame Size
nbits = reader.getBits(5)

Logger ("Nbits" + str(nbits))

x_min = reader.getBits(nbits)
x_max = reader.getBits(nbits)
y_min = reader.getBits(nbits)
y_max = reader.getBits(nbits)
reader.bitclear()

Logger("RECT(x20 pixels):" + str(x_min)  + "," + str(y_min)   + "," + str(x_max)   + "," + str(y_max)    )

framerate = reader.getUINT16()
framecount = reader.getUINT16()

Logger( str(framerate)  + "fps  count:" + str(framecount))

タグの読み出し

タグを読むだけなので、ENDタグが来るまで、ひたすら空読みを繰り返します。

    def getTag(self):
        num = self.getUINT16()
        tag = (num >> 6) & 0x3ff
        if ( num & 0x3f == 0x3f):
            length = self.getUINT32()
        else:
            length = num & 0x3f
        return (tag,length)


#Read TAG
while True:
    (tag,length) = reader.getTag()
    Logger( TAG[tag].__name__ + ":" + str(length) + "byte")
    reader.getBytes(length)
    if tag == 0: #END Tag
        break

TAGリスト

Valueを関数にしてあるので、ダミー関数をインプリメントしてあります(一応タグの内容も解析する気なのです)。面倒なら''でくくって文字列で返すといいです。

TAG = {
    0:End,
    1:ShowFrame,
    2:DefineShape,
    4:PlaceObject,
    5:RemoveObject,
    6:DefineBits,
    7:DefineButton,
    8:JPEGTables,
    9:SetBackgroundColor,
    10:DefineFont,
    11:DefineText,
    12:DoAction,
    13:DefineFontInfo,
    14:DefineSound,
    15:StartSound,
    17:DefineButtonSound,
    18:SoundStreamHead,
    19:SoundStreamBlock,
    20:DefineBitsLossless,
    21:DefineBitsJPEG2,
    22:DefineShape2,
    23:DefineButtonCxform,
    24:Protect,
    26:PlaceObject2,
    28:RemoveObject2,
    32:DefineShape3,
    33:DefineText2,
    34:DefineButton2,
    35:DefineBitsJPEG3,
    36:DefineBitsLossless2,
    37:DefineEditText,
    39:DefineSprite,
    43:FrameLabel,
    45:SoundStreamHead2,
    46:DefineMorphShape,
    48:DefineFont2,
    56:ExportAssets,
    57:ImportAssets,
    58:EnableDebugger,
    59:DoInitAction,
    60:DefineVideoStream,
    61:VideoFrame,
    62:DefineFontInfo2,
    64:EnableDebugger2,
    65:ScriptLimits,
    66:SetTabIndex,
    69:FileAttributes,
    70:PlaceObject3,
    71:ImportAssets2,
    73:DefineFontAlignZones,
    74:CSMTextSettings,
    75:DefineFont3,
    76:SymbolClass,
    77:Metadata,
    78:DefineScalingGrid,
    82:DoABC,
    83:DefineShape4,
    84:DefineMorphShape2,
    86:DefineSceneAndFrameLabelData,
    87:DefineBinaryData,
    88:DefineFontName,
    89:StartSound2,
    90:DefineBitsJPEG4,
    91:DefineFont4, 
    93:EnableTelemetry
}
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3