I have been inbetween projects the last weeks and thought i could do something productive while i’m idle, so i created as3swf, a low level Actionscript 3 library to parse, create, modify and publish SWF files.
At the same time i experimented with iPhone development, as we at côdeazur have some commercial iPhone work lined up. Part of those experiments involved Core Graphics/Quartz programming, and the old grumpy Flash developer in me immediately thought it would be cool if i could reuse Flash vector art in my iPhone applications. More details on that here: Shape Export to Objective-C.
With as3swf (which parses SWF shapes down to the bone) i already had a powerful weapon in my hands. I’d “just” had to extend it to generate Objective-C source code from a SWF shape. SWF shape records translate almost directly to AS3 drawing API calls (so i thought), and thus also to the similar Quartz Core Graphics API calls. I couldn’t have been more wrong.
So, for the record, SWF shape records kinda translate to AS3 drawing API calls, but not really – there are quite a few nuts to crack along the way. It is not as easy as it initially sounds, and little of this is officially documented or explained in Adobe’s SWF10 specification (or anywhere else for that matter), so i thought i document my findings here.
One blog post was brought to my attention, that helped me understand how the Flash Player actually draws shapes: Mike Swanson’s “Converting Flash Shapes to WPF” (ironically, Mike is Technical Evangelist at Microsoft). A very good read, i learned much of the stuff i’m going to explain here through that post.
SWF Shapes
I don’t want to go into much detail about the SWF10 file format in general here, it is sufficiently well documented by Adobe. However, an introduction to Flash shapes and how they are stored in SWF seems appropriate.
Shapes are represented in SWF by DefineShape tags. Let’s jump right into practice and draw a simple shape on stage in the Flash IDE.
Create a new FLA. Select the Rectangle tool, choose a 10px red stroke and a solid gray fill, and draw a rectangle:

Publish the SWF, load it into a ByteArray, have as3swf parse it, and dump the results to the console (example code here). The output should contain a DefineShape4 tag that looks something like this:
[83:DefineShape4] ID: 1
FillStyles:
[1] [SWFFillStyle] Type: 0 (solid), Color: 666666
LineStyles:
[1] [SWFLineStyle2] Width: 200, Color: ff0000
ShapeRecords:
[SWFShapeRecordStyleChange] MoveTo: 400,400, FillStyle1: 1, LineStyle: 1
[SWFShapeRecordStraightEdge] Horizontal: 2000
[SWFShapeRecordStraightEdge] Vertical: 2000
[SWFShapeRecordStraightEdge] Horizontal: -2000
[SWFShapeRecordStraightEdge] Vertical: -2000
[SWFShapeRecordEnd]
DefineShape contains a set of initial style definitions (in this case that’s the dark gray solid fill and the 10px red stroke), followed by a set of shape records describing the geometry of the shape. Note that all coordinates are measured in “Twips” (a 20th of a pixel) so you have to divide all of them by 20 to get to the pixel coordinates.
Shape records can come in four flavors:
StyleChange records can have more than one purpose. They move the drawing cursor to a new position (equivalent to moveTo in the drawing API), set line and fill styles for following edges (equivalent to beginFill and lineStyle in the drawing API), and define new line and fill styles where needed.
StraightEdge and CurvedEdge records define the actual edges of the shape (equivalent to lineTo and curveTo in the drawing API).
The End record is always the last shape record, it simply tells the Flash Player that the shape ends there.
Simple stuff you might think. I haven’t told you about FillStyle0 and FillStyle1 yet, though.
FillStyle0 and FillStyle1
This is where things get twisted. Edges in SWF shapes can have two different fills.
Create a new FLA, select the Rectangle tool, choose a 10px red stroke and no fill, and draw two rectangles, the second one completely enclosed by the first one. Then use the Paint Bucket tool to fill the donut part with solid dark gray:

The as3swf dump of the resulting SWF contains the following DefineShape tag:
[83:DefineShape4] ID: 1
FillStyles:
[1] [SWFFillStyle] Type: 0 (solid), Color: 666666
LineStyles:
[1] [SWFLineStyle2] Width: 200, Color: ff0000
ShapeRecords:
[SWFShapeRecordStyleChange] MoveTo: 1800,1800, FillStyle0: 1, LineStyle: 1
[SWFShapeRecordStraightEdge] Horizontal: -800
[SWFShapeRecordStraightEdge] Vertical: -800
[SWFShapeRecordStraightEdge] Horizontal: 800
[SWFShapeRecordStraightEdge] Vertical: 800
[SWFShapeRecordStyleChange] MoveTo: 2400,400, FillStyle0: 0, FillStyle1: 1
[SWFShapeRecordStraightEdge] Vertical: 2000
[SWFShapeRecordStraightEdge] Horizontal: -2000
[SWFShapeRecordStraightEdge] Vertical: -2000
[SWFShapeRecordStraightEdge] Horizontal: 2000
[SWFShapeRecordEnd]
The shape records define the inner rectangle first, then the outer rectangle.
It is important to note that in this example, both rectangles are “drawn” clockwise. The shape starts with the inner rectangle’s bottom right corner, then moves 40px left, 40px up, 40px right, and 40px down, back to the origin. It then moves to the right top corner of the outer rectangle, moves 100px down, 100px left, 100px up, and 100px right, back to the outer rectangle’s origin.
The interesting parts of this shape structure are the two StyleChange records and the FillStyle information they contain: FillStyle0 and FillStyle1 are 1-based indices into the array of FillStyles, defining the fills of the following (sub-)shapes. FillStyle0 defines the fill to the left side of an edge, FillStyle1 defines the fill to the right side of an edge.
Lets look at the first StyleChange record. It defines the style properties of the inner rectangle. FillStyle0 is set to 1 (our solid dark gray fill), FillStyle1 is missing and thus defaults to 0 (no fill). As the rectangle edges draw clockwise, FillStyle0 defines the fill on the outside of the rectangle. FillStyle1 defines the inside fill, which in our case is transparent.
Moving on to the second StyleChange record. It defines the style properties for the outer rectangle. FillStyle0 is reset to 0 (no fill), FillStyle1 is set to 1 (solid dark gray fill). The rectangle edges again draw clockwise, so FillStyle0 defines the outside fill (transparent), and FillStyle1 the inside fill (dark gray).
Now let’s add some more juice and look at a third, slightly more complex example.
Create a new FLA, select the Rectangle tool, choose a 10px red stroke and no fill, and draw two intersecting rectangles. Then use the Paint Bucket tool to fill each of the resulting three closed areas with a different color. I chose light gray, dark gray and black for this example:

Note how the intersecting edges are split in two. Fills and edges never overlap in individual SWF shapes. Ever.
The resulting SWF shape dump looks like this:
[83:DefineShape4] ID: 1
FillStyles:
[1] [SWFFillStyle] Type: 0 (solid), Color: 666666
[2] [SWFFillStyle] Type: 0 (solid), Color: 999999
[3] [SWFFillStyle] Type: 0 (solid), Color: 000000
LineStyles:
[1] [SWFLineStyle2] Width: 200, Color: ff0000
ShapeRecords:
[SWFShapeRecordStyleChange] MoveTo: 2400,1400, FillStyle1: 3, LineStyle: 1
[SWFShapeRecordStraightEdge] Horizontal: 1000
[SWFShapeRecordStraightEdge] Vertical: 2000
[SWFShapeRecordStraightEdge] Horizontal: -2000
[SWFShapeRecordStraightEdge] Vertical: -1000
[SWFShapeRecordStyleChange] FillStyle1: 2
[SWFShapeRecordStraightEdge] Horizontal: -1000
[SWFShapeRecordStraightEdge] Vertical: -2000
[SWFShapeRecordStraightEdge] Horizontal: 2000
[SWFShapeRecordStraightEdge] Vertical: 1000
[SWFShapeRecordStyleChange] FillStyle0: 3, FillStyle1: 1
[SWFShapeRecordStraightEdge] Vertical: 1000
[SWFShapeRecordStraightEdge] Horizontal: -1000
[SWFShapeRecordStyleChange] FillStyle0: 2
[SWFShapeRecordStraightEdge] Vertical: -1000
[SWFShapeRecordStraightEdge] Horizontal: 1000
[SWFShapeRecordEnd]
The first noteable difference to the previous two example are the FillStyle definitions. We use three different fills for this shape, so three FillStyles are defined in the header.
The first two sets of shape records define the outer edges of our shape (all edges are again drawn clockwise in this example). The first four edge records define the outer edges of the black shape (FillStyle1 is set to 3, referencing black for the inner fill), the following four edge records define the outer edges of the light gray shape (FillStyle1 is set to 2, referencing light gray for the inner fill).
The last two sets of edges are the interesting ones. They define the inner edges of our shape (the intersection area), whose fill is dark gray. And sure enough, FillStyle1 is set to 1, referencing the dark gray FillStyle for the inner fill. What’s different here is that FillStyle0 is also set, first to 3 (black), then to 2 (light gray), defining the outer fills for those edges (remember, we’re drawing clockwise, and FillStyle0 defines the fill to the left of the edge).
To recap: fills and edges in SWF shapes never intersect, and edges can have two fill styles. With these little tricks, SWF shapes save some file size, as each edge is only defined once.
The drawback is that raw SWF shapes need some tweaking in order to be redrawn using Actionscript and the Drawing API, which doesn’t support those concepts. Luckily, i did all the heavy lifting for you already in as3swf. The TagDefineShape class features a export() method, which dissects the shape in question and outputs code that is compatible with the AS3 Drawing API and Apple’s Quartz framework.
Draw fills first
Fills and strokes need to be drawn separately (fills first, strokes last). We focus on fills for now.
Let’s look at the third example again. In order to redraw that shape using the AS3 Drawing API, we need to draw three closed paths with three different fills. Now how do we get from the rather compressed SWF shape format to shapes that can be drawn using AS3?
Mike Swanson explains that in his blog post “Converting Flash Shapes to WPF” quite well.
We look at the FillStyle indices of each StyleChange record. If FillStyle1 is set to anything other than 0, we add the following edge records as is to our new edge list, along with the FillStyle info referenced by FillStyle1. If FillStyle0 is set to anything other than 0, we add the following edge records to the list in reversed order, along with the FillStyle info referenced by FillStyle0.
This way, if both FillStyle0 and FillStyle1 are set in a StyleChange record, we end up adding the edges that are following twice, once for each of the two fills. In our example, we end up with 16 edges instead of the 12 that are defined in the SWF.
All that’s left is sorting the edges by FillStyle, i.e.first draw edges with black fill, then draw edges with light gray fill, and finally draw edges with dark gray fill. Voilá, we got fill!
Draw strokes last
Last but not least we have to draw the strokes. This really got me puzzled for a while. To draw strokes in the correct order, we need to know how the Flash Player draws them internally, otherwise we might end up getting artefacts at end points.
Of course this is not documented anywhere, so we need to investigate. A very good start is the Flash IDE (i am using CS4 btw. Your mileage may vary if you use CS3 or older versions of the IDE). Create a new FLA, and draw some lines:

Select the Line tool, choose a 10px red stroke, and draw a horizontal line.
Then, choose a 20px blue stroke, and draw a vertical line that intersects the red line. Note how the blue line is drawn on top of the red line, as we would expect.
Now choose the 10px red stroke again, and draw a horizontal line that intersects the blue line. At first everything seems right, the second red line appears on top of the blue line, as we would expect. Until you deselect the lines by clicking somewhere on stage, that is. The red line moves below the blue line. WTF?
So let’s publish the SWF and take a look inside:
[83:DefineShape4] ID: 1
LineStyles:
[1] [SWFLineStyle2] Width: 200, Color: ff0000
[2] [SWFLineStyle2] Width: 400, Color: 0000ff
Shapes:
[SWFShapeRecordStyleChange] MoveTo: 1000,1000, LineStyle: 2
[SWFShapeRecordStraightEdge] Vertical: 800
[SWFShapeRecordStyleChange] LineStyle: 1
[SWFShapeRecordStraightEdge] Horizontal: 1400
[SWFShapeRecordStyleChange] MoveTo: 400,1800
[SWFShapeRecordStraightEdge] Horizontal: 600
[SWFShapeRecordStyleChange] LineStyle: 2
[SWFShapeRecordStraightEdge] Vertical: 600
[SWFShapeRecordStyleChange] MoveTo: 1000,400
[SWFShapeRecordStraightEdge] Vertical: 600
[SWFShapeRecordStyleChange] LineStyle: 1
[SWFShapeRecordStraightEdge] Horizontal: 1400
[SWFShapeRecordStyleChange] MoveTo: 400,1000
[SWFShapeRecordStraightEdge] Horizontal: 600
[SWFShapeRecordEnd]
Examining the shape records, we find that the order of edges is totally messed up. There is no way to find out what is drawn first from the order of the edges in a SWF. Apparently, the Flash Player uses some other way of sorting stroked edges before it draws them.
The Flash IDE gave us a clue though. Lines with the same LineStyle seem to be drawn on the same layer. This is confirmed by the order of LineStyles in the SWF: the first LineStyle is the 10px red stroke (drawn first, thus appearing below everything else), the second LineStyle is the 20px blue stroke (drawn last, thus appearing in front). This suggests that stroked edges are sorted by LineStyle index.
Sure enough, this seems to be the case. We thus need to sort edges by LineStyle index and draw them in that order.
BAZINGA!
We now have knowledge about all the theory to redraw SWF shapes in AS3, Objective-C or any other language/platform, and as3swf provides us with the implementation.
Go create!
I’ll leave you with the Drawing API code for the fancy côdeazur logo, generated directly from SWF:
// Fills:
graphics.beginFill(0x000000);
graphics.moveTo(132.200000, 38.200000);
graphics.curveTo(146.150000, 44.950000, 156.050000, 56.950000);
graphics.curveTo(146.450000, 70.000000, 143.150000, 85.600000);
graphics.curveTo(138.200000, 73.000000, 127.100000, 65.200000);
graphics.curveTo(115.550000, 57.250000, 101.600000, 57.250000);
graphics.curveTo(83.150000, 57.250000, 70.100000, 70.300000);
graphics.curveTo(57.050000, 83.350000, 57.050000, 101.800000);
graphics.curveTo(57.050000, 120.250000, 70.100000, 133.300000);
graphics.curveTo(83.150000, 146.350000, 101.600000, 146.350000);
graphics.curveTo(115.550000, 146.350000, 127.100000, 138.400000);
graphics.curveTo(138.200000, 130.600000, 143.150000, 118.000000);
graphics.curveTo(146.450000, 133.600000, 156.050000, 146.650000);
graphics.curveTo(146.150000, 158.650000, 132.200000, 165.400000);
graphics.curveTo(117.800000, 172.300000, 101.600000, 172.300000);
graphics.curveTo(72.350000, 172.300000, 51.800000, 151.750000);
graphics.curveTo(31.100000, 131.050000, 31.100000, 101.800000);
graphics.curveTo(31.100000, 72.550000, 51.800000, 52.000000);
graphics.curveTo(72.350000, 31.300000, 101.600000, 31.300000);
graphics.curveTo(117.800000, 31.300000, 132.200000, 38.200000);
graphics.beginFill(0x86b9e1);
graphics.moveTo(128.750000, 74.650000);
graphics.curveTo(140.000000, 85.900000, 140.000000, 101.800000);
graphics.curveTo(140.000000, 117.850000, 128.750000, 128.950000);
graphics.curveTo(117.650000, 140.200000, 101.600000, 140.200000);
graphics.curveTo(85.550000, 140.200000, 74.300000, 128.950000);
graphics.curveTo(63.050000, 117.700000, 63.050000, 101.800000);
graphics.curveTo(63.050000, 85.900000, 74.300000, 74.650000);
graphics.curveTo(85.550000, 63.400000, 101.600000, 63.400000);
graphics.curveTo(117.650000, 63.400000, 128.750000, 74.650000);
graphics.moveTo(118.700000, 84.700000);
graphics.curveTo(111.650000, 77.500000, 101.600000, 77.500000);
graphics.curveTo(91.550000, 77.500000, 84.500000, 84.700000);
graphics.curveTo(77.300000, 91.750000, 77.300000, 101.800000);
graphics.curveTo(77.300000, 111.850000, 84.500000, 118.900000);
graphics.curveTo(91.550000, 126.100000, 101.600000, 126.100000);
graphics.curveTo(111.650000, 126.100000, 118.700000, 118.900000);
graphics.curveTo(125.900000, 111.850000, 125.900000, 101.800000);
graphics.curveTo(125.900000, 91.750000, 118.700000, 84.700000);
graphics.beginFill(0x000000);
graphics.moveTo(288.800000, 101.500000);
graphics.lineTo(288.800000, 172.600000);
graphics.lineTo(262.550000, 172.600000);
graphics.lineTo(262.700000, 101.650000);
graphics.curveTo(262.550000, 83.200000, 249.500000, 70.150000);
graphics.curveTo(236.450000, 57.100000, 218.000000, 57.100000);
graphics.curveTo(199.550000, 57.100000, 186.500000, 70.300000);
graphics.curveTo(173.300000, 83.350000, 173.300000, 101.800000);
graphics.curveTo(173.300000, 120.250000, 186.350000, 133.300000);
graphics.curveTo(199.400000, 146.350000, 217.850000, 146.500000);
graphics.lineTo(256.850000, 146.500000);
graphics.lineTo(256.850000, 172.600000);
graphics.lineTo(217.700000, 172.600000);
graphics.curveTo(188.450000, 172.450000, 167.900000, 151.750000);
graphics.curveTo(147.200000, 131.050000, 147.200000, 101.800000);
graphics.curveTo(147.200000, 72.400000, 167.900000, 51.700000);
graphics.curveTo(188.600000, 31.000000, 218.000000, 31.000000);
graphics.curveTo(247.250000, 31.000000, 267.950000, 51.700000);
graphics.curveTo(288.650000, 72.400000, 288.800000, 101.500000);
graphics.beginFill(0x86b9e1);
graphics.moveTo(242.600000, 101.950000);
graphics.lineTo(242.600000, 101.650000);
graphics.curveTo(242.450000, 91.450000, 235.250000, 84.400000);
graphics.curveTo(228.050000, 77.200000, 218.000000, 77.200000);
graphics.curveTo(207.950000, 77.200000, 200.600000, 84.400000);
graphics.curveTo(193.400000, 91.750000, 193.400000, 101.800000);
graphics.curveTo(193.400000, 111.850000, 200.600000, 119.050000);
graphics.curveTo(207.650000, 126.250000, 217.850000, 126.400000);
graphics.lineTo(242.450000, 126.400000);
graphics.lineTo(242.450000, 102.550000);
graphics.lineTo(242.600000, 101.950000);
graphics.moveTo(218.000000, 62.950000);
graphics.curveTo(234.050000, 62.950000, 245.450000, 74.200000);
graphics.curveTo(256.850000, 85.600000, 256.850000, 101.650000);
graphics.lineTo(256.850000, 140.650000);
graphics.lineTo(217.850000, 140.650000);
graphics.curveTo(201.800000, 140.650000, 190.400000, 129.250000);
graphics.curveTo(179.150000, 117.850000, 179.150000, 101.800000);
graphics.curveTo(179.150000, 85.750000, 190.550000, 74.350000);
graphics.curveTo(201.950000, 62.950000, 218.000000, 62.950000);
graphics.endFill();