I have already written most important part about SWF format and
Erlang's bit syntax
at previous
post. But just for the record, I take a note of miscellaneous
knowledge to construct an interesting Flash shape. This entire note
doesn't have any practical value because there are already many other
useful libraries to build a swf file. But some of historical esoteric
magical peculiar internal might interests you.
In the previous example, I only made three control tags:
- End (Tag type = 0)
- ShowFrame (Tag type = 1)
- SetBackgroundColor (Tag type = 9)
Here, I introduce you two additional tags,
- DefineShape (Tag type = 2) defines a vector graphics, and
- PlaceObject2 (Tag type = 26) shows the graphics to an actual screen.
I designed new data structure to represent a SWF shape. While
spec/0 is almost same as previous program, I add sample_shape/0 to
define DefineShape.
% an example shape.
spec() ->
{{frame_size, 6000, 3000},
{frame_rate, 16},
{frame_count, 160},
[{set_background_color, {rgb, 240, 250, 250}},
sample_shape(),
{place_object2, 1},
{show_frame},
{end_tag}]}.
sample_shape() ->
{define_shape,
{shape_id, 1},
{bounds, {rectangle, 2000, 3000, 1000, 2000}},
{shapes, {{fills, [{solid_fill, {rgb, 50, 200, 50}}]},
{lines, [{line_style, 100, {rgb, 100, 255, 100}},
{line_style, 100, {rgb, 50, 150, 50}}]},
{shape_records,
[{style_change, [{move_to, 2000, 2000}]},
{style_change, [{line_style, 1},
{fill_style0, 1}]},
{straight_edge, 0, -1000},
{straight_edge, 1000, 0},
{style_change, [{line_style, 2},
{fill_style0, 1}]},
{straight_edge, 0, 1000},
{straight_edge, -1000, 0},
{end_shape}]
}}}}.
It looks complicated though, it is actually very
straightforward. A DefineShape has a shape ID (or character ID), its
bounding box, and actual shapes. "shape_id", "bounds", and "shapes"
tuples at sample_shape/0 denote these information. Shape ID is used
later for a reference by PlaceObject2.
A Shapes part consists of further sub elements, fills, lines and
shape records. Every style information are defined in advance, and
referenced with corresponding ID. In this case, one solid fill and two
line styles are defined. After styles are defined, shape records are
followed. A shape record is the most interesting and complicated
part. I'll draw a moss green rectangle in 1000 x 1000 twips (20 twips
= 1 pixel) with light and dark green borders in a pseudo 3-D
style.
This is an actual implementation of DefineShape and
PlaceObject2. I'm afraid it seems to be complicated, but it is just a
direct translation from the specification. Thanks to Erlang's pattern
matching syntax, It is easy to add a new tag by putting a new
pattern. But sometimes distinction between ; (semi colon is used
between patterns in a function) and . (dot is used after a function
definition) annoys me.
tag({define_shape,
{shape_id, ShapeId},
{bounds, Bounds},
{shapes, ShapesWithStyle}}) ->
BoundsBits = rectangle(Bounds),
Shapes = shape_with_style(ShapesWithStyle),
record_header_body(2, list_to_binary([<<ShapeId:16/unsigned-little>>, BoundsBits, Shapes]));
tag({place_object2, CharacterId}) ->
PlaceFlagHasClipActions = 0,
PlaceFlagHasClipDepth = 0,
PlaceFlagHasName = 0,
PlaceFlagHasRatio = 0,
PlaceFlagHasColorTransform = 0,
PlaceFlagHasMatrix = 0,
PlaceFlagHasCharacter = 1,
PlaceFlagMove = 0,
Depth = 1,
record_header_body(26, <<PlaceFlagHasClipActions:1,
PlaceFlagHasClipDepth:1,
PlaceFlagHasName:1,
PlaceFlagHasRatio:1,
PlaceFlagHasColorTransform:1,
PlaceFlagHasMatrix:1,
PlaceFlagHasCharacter:1,
PlaceFlagMove:1,
Depth:16/unsigned-little,
CharacterId:16/unsigned-little>>).
shape_with_style/1 is used to construct SHAPEWITHSTYLE structure
in the specification.
shape_with_style({{fills, Fills}, {lines, Lines}, {shape_records, Shapes}}) ->
FillBits = nbits_unsigned([length(Fills)]),
LineBits = nbits_unsigned([length(Lines)]),
ShapeRecords = shape_records({shape_records, Shapes}, FillBits, LineBits),
[fill_style_array({fills, Fills}),
line_style_array({lines, Lines}),
<<FillBits:4,
LineBits:4>>,
ShapeRecords].
The tricky part is FillBits and LineBits. As I have already
mentioned when explaining a rectangle format in previous post, the SWF
format tries to shorten a file size by all means, bit by bit. FillBits
defines maximum bit size needed by fill style id. For example, if you
use only one fill style, one is enough for FillBits. But if you use
seven, FillBits is three (because seven is 111 as a binary). After
FillBits and LineBits are calculated, they are needed in next helper
function shape_records/3. But before I show you shape_records/3, some
simple functions to construct styles are followed.
fill_style_array({fills, FillStyleArray}) ->
FillStyleCount = length(FillStyleArray),
[<<FillStyleCount:8>>, [fill_style(X) || X <- FillStyleArray]].
fill_style({solid_fill, RGB}) -> [<<16#00:8>>, rgb(RGB)].
line_style_array({lines, LineStyleArray}) ->
LineStyleCount = length(LineStyleArray),
[<<LineStyleCount:8>>, [line_style(X) || X <- LineStyleArray]].
line_style({line_style, Width, RGB}) ->
[<<Width:16/unsigned-little>>, rgb(RGB)].
I think these are straightforward. Perhaps, next is the most
tricky part of the SWF shape. Shape records include a number of
records, each record represents style change or position change or
so. While a rectangle format must to be byte aligned, each record must
not be aligned, but whole shape records including them must be
aligned! This fact is described in the specification very obscure way,
so I didn't find out what to do unless many try-and-errors. Although,
I might misunderstood what is a shape record...
Each individual shape record is byte-aligned within an array of shape records; one shape
record is padded to a byte boundary before the next shape record begins. --- SWF File Format Specification Version 10
To concatenate many bit strings into one, and align whole bits, I made
two functions padding/1 and concat_bits/1 (padding/1 is same one as
previous post). It is likely that concatenating bitstrings exists
already, but I couldn't find out from the library.
shape_records({shape_records, ShapeRecords}, FillBits, LineBits) ->
padding(concat_bit([shape_record(X, FillBits, LineBits) || X <- ShapeRecords])).
padding(Bits) ->
Padding = 8 - bit_size(Bits) rem 8,
<<Bits/bitstring, 0:Padding>>.
concat_bit(XS) -> lists:foldl(fun(X, Y) -> <<Y/bitstring, X/bitstring>> end, <<>>, XS).
Rest of the source is definitions of each record. I omitted a lot of
features to the explanation purpose. EndShapeRecord and
StraightEdgeRecord are quite simple.
shape_record({end_shape}, _, _) -> <<0:1, 0:5>>;
shape_record({straight_edge, DeltaX, DeltaY}, _, _) ->
NumBits = nbits_signed([DeltaX, DeltaY]),
GeneralLineFlag = 1,
<<1:1, 1:1, (NumBits - 2):4, GeneralLineFlag:1, DeltaX:NumBits, DeltaY:NumBits>>;
But StyleChangeRecord is the second tricky and daunting part in
spite of Erlang's succinct syntax. A part of reasons of the complexity
is that StyleChangeRecord allows to define a couple of operations at
once optionally; line style, fill style, and pen move. So we need some
data structure to represent such option.
In object oriented languages, a dictionary or a map is commonly
used while an associated list is popular in functional languages. In
Erlang, a list of tuples might be enough. lists:keysearch/3 find a
tuple in a list where first argument is matched, so we can use it as a
dictionary like structure. In our example, [{line_style, 1},
{fill_style0, 1}] means we want to change line style and fill style
same time. and lists:keysearch(line_style, 1, ChangeSpec) detects a
line style change option.
shape_record({style_change, ChangeSpec}, FillBits, LineBits) ->
TypeFlag = 0,
StateNewStyles = 0,
StateFillStyle1 = 0,
{StateLineStyle, LineStyleData} =
case lists:keysearch(line_style, 1, ChangeSpec) of
{value, {_, LineStyle}} -> {1, <<LineStyle:LineBits>>};
false -> {0, <<>>} end,
{StateFillStyle0, FillStyle0Data} =
case lists:keysearch(fill_style0, 1, ChangeSpec) of
{value, {_, FillStyle0}} -> {1, <<FillStyle0:FillBits>>};
false -> {0, <<>>} end,
{StateMoveTo, MoveData} =
case lists:keysearch(move_to, 1, ChangeSpec) of
{value, {_, MoveDeltaX, MoveDeltaY}} ->
MoveBits = nbits_signed([MoveDeltaX, MoveDeltaY]),
{1, <<MoveBits:5,
MoveDeltaX:MoveBits/signed,
MoveDeltaY:MoveBits/signed>>};
false -> {0, <<>>} end,
<<TypeFlag:1,
StateNewStyles:1,
StateLineStyle:1,
StateFillStyle1:1,
StateFillStyle0:1,
StateMoveTo:1,
MoveData/bitstring,
FillStyle0Data/bitstring,
LineStyleData/bitstring>>.
I uploaded final
program
http://languagegame.org/pub/swf.erl.