今回は、2020年に終了が確定して居るAdobe Flash Plyerで使われているSWFファイルを読んでみます。10年ぐらい前に流行ったFlashアニメーションをレスキューしたいのが動機ですけどね(ただし、やりたいと出来るは別の話。)
SWFの仕様書を読むとオープンソースが作れないライセンスの問題は既に解消されている様なので、とりあえずタグだけ読んでみました。
用意するモノ
- Adobe SWF フォーマット V19 仕様書
- Python 2.7
なに英語だと(苦手)。日本語に訳したヤツの方が意味不明な事が多いから気にしない。
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
}