Categories
Articles

Read Flash SWF Header in AIR with Parsley Framework

I was asked to look for open source tools to read the basic information contained in a Flash swf file. Most of the items you find are for reverse engineering swf files. In the search process I found two sources to parse the swf file header.

The first was a php script written by Carlos Falo Hervá. You need to scroll down to the end of the post to find his work. This works just fine if you want to run off a server.

The second is written in Actionscript for Flash CS5. Only the author’s handle, jared, and not the author’s name is not available at the site. The download works nice and you can run from the author’s web page. You need type or paste in a url for the swf file name. The code is all dropped in on the first frame.

This site also provided a nice link to the Adobe SWF file version 10 specifications if you care to know. Page 25 has the key information on the SWF header. This is also repeated in the SWFHeaderParser parser class presented in this post.

I took the code for the second example and created a drag and drop version Adobe Air. First I separated the code into two Actionscript classes. One for parsing and one for loading swf file bytes. These classes are named respectively SWFHeaderParser and SWFHeaderLoader. I added a value object, SWFHeader_vo, to pass around the values.

I glued those items together in an Air project using the Parsley framework. This gave me a chance to use Parsley DynamicObject for the SWFHeaderLoader class that loads the SWF file for the parser.

The result is a fully decoupled version that serves as a super example of model-view-controller using Parsley for an Air app.

Download files:
Here is the Flex project if you want to explore the code and the Air installer in case you just want the tool.

[ad name=”Google Adsense”]

SWF Parser – SWFHeaderParser.as
The parser class is first as that is what you might want to integrate into your code. Note it returns the SWFHeader_vo class. The comments are scant in this class since I did write the code and only converted it into a class, returned a value object and removed code not specific to parsing the bytes. The value object is created and populated on lines 85 – 95.

package swf
{
	import flash.geom.Rectangle;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import vo.SWFHeader_vo;
	/**
	 * SWF Header parsing. Code adapted from http://simplistika.com/parsing-reading-swf-header/.
	 * */
	public class SWFHeaderParser
	{
		private var xByte : uint;
		private var xNBits : int;
		private var xOffset : int;
		public function SWFHeaderParser()
		{
		}
		/**
		 * Get a SWFHeader_vo object from a ByteArray
		 * @param swfByteArray Any byte array, but expecting a SWF binary file or header from SWF file.
		 * @return Parsed values in SWFHeader_vo object.
		 * */
		public function parseBytes(swfByteArray : ByteArray) : SWFHeader_vo
		{
			return fParse(swfByteArray);
		}
		/**
		 * Original code from http://simplistika.com/parsing-reading-swf-header/ changed to 
		 * return SWFHeader_vo object.
		 * */
		private function fParse(v : ByteArray) : SWFHeader_vo
		{
			/*
			Field		Type 	Comment
								Signature byte
			Signature	UI8		"F" indicates uncompressed
								"C" indicates compressed (SWF 6 and later only)
			Signature	UI8		Signature byte always "W"
			Signature	UI8		Signature byte always "S"
			
			Version		UI8		Single byte file version
			FileLength	UI32	Length of entire file in bytes
			FrameSize	RECT	Frame size in twips
			FrameRate	UI16	Frame delay in 8.8 fixed number of frames per second
			FrameCount	UI16	Total number of frames in file
			The header begins with a three-byte signature of either 0×46, 0×57, 0×53 ("FWS"); or 0×43, 0×57, 0×53 (“CWS”). An FWS signature indicates an uncompressed SWF file; CWS indicates that the entire file after the first 8 bytes (that is, after the FileLength field) was compressed by using the ZLIB open standard. The data format that the ZLIB library uses is described by Request for Comments (RFCs) documents 1950 to 1952. CWS file compression is permitted in SWF 6 or later only.
			
			A one-byte version number follows the signature. 
			The version number is not an ASCII character, 
			but an 8-bit number. For example, for SWF 4, the version byte is 0×04, not the 
			ASCII character “4? (0×34). The FileLength field is the total length of the 
			SWF file, including the header. If this is an uncompressed SWF file (FWS signature), 
			the FileLength field should exactly match the file size. If this is a compressed SWF file 
			(CWS signature), the FileLength field indicates the total length of the file after decompression, 
			and thus generally does not match the file size. Having the uncompressed size available can make 
			the decompression process more efficient. The FrameSize field defines the width and height 
			of the on-screen display. This field is stored as a RECT structure, meaning that its size 
			may vary according to the number of bits needed to encode the coordinates. The FrameSize 
			RECT always has Xmin and Ymin value of 0; the Xmax and Ymax members define the width and 
			height. The FrameRate is the desired playback rate in frames per second.
			
			Source: http://simplistika.com/parsing-reading-swf-header/
			
			*/
			var vFormat : String;
			var vSwfVersion : int;
			var vFileLength : int;
			var vFrameRate : int;
			var vTotalFrames : int;
			var vFrameSize : Rectangle;
			v.endian = Endian.LITTLE_ENDIAN;
			vFormat = v.readUTFBytes(3);
			vSwfVersion = v.readByte();
			vFileLength = v.readUnsignedInt();
			v.readBytes(v);
			v.length -= 8;
			if (vFormat == "CWS")
				v.uncompress();
			v.position = 0;
			vFrameSize = new Rectangle();
			vFrameSize.left = xfReadNBits(v, true) / 20;
			vFrameSize.right = xfReadNBits(v) / 20;
			vFrameSize.top = xfReadNBits(v) / 20;
			vFrameSize.bottom = xfReadNBits(v) / 20;
			vFrameRate = v.readUnsignedByte() / 256 + v.readUnsignedByte();
			vTotalFrames = v.readUnsignedShort();
			var swfHeader_vo:SWFHeader_vo = new SWFHeader_vo();
			swfHeader_vo.format = vFormat;
			swfHeader_vo.swfVersion = vSwfVersion;
			swfHeader_vo.sizeUncompressed = vFileLength;
			swfHeader_vo.width = vFrameSize.width;
			swfHeader_vo.height =vFrameSize.height;
			swfHeader_vo.frameRate = vFrameRate;
			swfHeader_vo.totalFrames = vTotalFrames;
			return swfHeader_vo;
		}
		/**
		 * Original code from http://simplistika.com/parsing-reading-swf-header/.
		 * */
		private function xfReadNBits(v : ByteArray, vStart : Boolean = false) : uint
		{
			var n : uint;
			if (vStart)
			{
				xByte = v.readUnsignedByte();
				xNBits = xByte >> 3;
				xOffset = 3;
			}
			n = xByte << (32 - xOffset) >> (32 - xNBits);
			xOffset -= xNBits;
			while (xOffset < 0)
			{
				xByte = v.readUnsignedByte();
				n |= (xOffset < -8) ? (xByte << (-xOffset - 8)) : (xByte >> (-xOffset - 8));
				xOffset += 8;
			}
			return n;
		}
	}
}

SWF Loader – SWFHeaderLoader.as
This is my class to load the SWF and it uses Parsley messaging. If you do not intend to use Parsley, you need to add your own messaging.

Most of this is standard URLLoader coding.

The SWFHeaderParser is called on line 62. On line 63, the actual bytes are not gotten from the SWFHeaderParser. So it can be a bit misleading that that is information found in the SWF header. The SWF header only contains the uncompressed file size.

package swf
{
	import events.SWFHeaderLoaderCompleteEvent;
	import events.SWFHeaderLoaderErrorEvent;
	import events.SWFHeaderLoaderProgressEvent;
	import flash.events.Event;
	import flash.events.HTTPStatusEvent;
	import flash.events.IOErrorEvent;
	import flash.events.ProgressEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import models.SWFHeaderModel;
	import vo.SWFHeader_vo;
	/**
	 * Byte loader for a SWF file.
	 * */
	public class SWFHeaderLoader 
	{
		/**
		 * Has a URLLoader
		 * */
		private var urlLoader:URLLoader;
		/**
		 * Model
		 * */
		[Inject]
		public var model:SWFHeaderModel;
		/**
		 * Receives Parsley messages.
		 * */
		[MessageDispatcher]
		public var dispatcher:Function;
		/**
		 * Load the bytes from a file.
		 * */
		public function load(url:String):void
		{
			urlLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, urlLoader_completeEventHandler);
			urlLoader.addEventListener(ProgressEvent.PROGRESS, urlLoader_progressHandler);
			urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, urlLoader_securityErrorHandler);
			urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, urlLoader_httpStatusHandler);
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR, urlLoader_ioErrorHandler);
			urlLoader.dataFormat = URLLoaderDataFormat.BINARY;
			urlLoader.load( new URLRequest(url) );
		}
		/**
		 * Handler for the urlLoader ProgressEvent.PROGRESS event.
		 * */
		private function urlLoader_progressHandler(event:ProgressEvent):void 
		{
			dispatcher(new SWFHeaderLoaderProgressEvent(SWFHeaderLoaderProgressEvent.PROGRESS, event.bytesLoaded, event.bytesTotal));
		}
		/**
		 * Handler for the urlLoader Event.COMPLETE event.
		 * */
		private function urlLoader_completeEventHandler(event:Event):void
		{
			var swfHeader_vo:SWFHeader_vo = new SWFHeader_vo();
			swfHeader_vo  = new SWFHeaderParser().parseBytes(urlLoader.data);
			swfHeader_vo.sizeCompressed = urlLoader.bytesLoaded;
			dispatcher(new SWFHeaderLoaderCompleteEvent(SWFHeaderLoaderCompleteEvent.COMPLETE, swfHeader_vo));
		}
		/**
		 * Handler for the urlLoader SecurityErrorEvent.SECURITY_ERROR event.
		 * */
		private function urlLoader_securityErrorHandler(event:SecurityErrorEvent):void 
		{
			dispatcher(new SWFHeaderLoaderErrorEvent(SWFHeaderLoaderErrorEvent.SECURITY_ERROR));
		}
		/**
		 * Handler for the urlLoader HTTPStatusEvent.HTTP_STATUS event.
		 * */
		private function urlLoader_httpStatusHandler(event:HTTPStatusEvent):void 
		{
			if (event.status != 200 && event.status != 0)
			{
				dispatcher(new SWFHeaderLoaderErrorEvent(SWFHeaderLoaderErrorEvent.HTTP_ERROR));
			}
		}
		/**
		 * Handler for the urlLoader IOErrorEvent.IO_ERROR event.
		 * */
		private function urlLoader_ioErrorHandler(event:IOErrorEvent):void 
		{
			dispatcher(new SWFHeaderLoaderErrorEvent(SWFHeaderLoaderErrorEvent.IO_ERROR));
		}
	}
}

SWF Header Value Object – SWFHeader_vo.as
The value object I use in the SWFHeaderLoader and SWFHeaderParser.

package vo
{
	/**
	 * Defines the values in the SWF file header record needed for app
	 * */
	[Bindable]
	public class SWFHeader_vo
	{
		public var format:String;			//	3 characters.
											//  First character:
											//  	"F" indicates uncompressed.
											// 		"C" indicates compressed (SWF 6 and later only).
											//  Second character always "W".
											//  Third character always "S".
		public var swfVersion:Number;		//	The swf version.
		public var frameRate:Number;		//	Frame rate.
		public var totalFrames:Number;		//	Number of frames on main timeline.
		public var width:Number;			//	Stage width.
		public var height:Number;			//	Stage height.
		public var sizeUncompressed:Number;	//	File size before compression.
		public var sizeCompressed:Number;	//	Actual file size.
	}
}

[ad name=”Google Adsense”]
The remainder code listings are the wiring into a Parsley Framework and published as an Adobe Air project.

Application Class – SWFHeader.mxml

This is the application mxml file. Gotta love how Parsley helps make these application mxml files minimal.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Application container
-->
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" 
					   xmlns:s="library://ns.adobe.com/flex/spark" 
					   xmlns:mx="library://ns.adobe.com/flex/mx"
					   xmlns:parsley="http://www.spicefactory.org/parsley" 
					   xmlns:views="views.*"
					   height = "450"
					   backgroundAlpha="0"
					   showStatusBar="false" 					>
	<fx:Declarations>
		<!-- Place non-visual elements (e.g., services, value objects) here -->
		<parsley:ContextBuilder config="ParsleyConfiguration"  />
		<parsley:Configure/>
	</fx:Declarations>
	<views:Main  />
</s:WindowedApplication>

Parsley Configuration File – ParsleyConfiguration.mxml
My first example that uses a Parsley DynamicObject. It is the SWFHeaderLoader class. I had a lot of silent failures before I could get this to stick. The result is that Parsley manages objects made from SWFHeaderLoader so we can have Parsley messaging and insertions. Only tried it with one object however.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Parsley framework configuration file
-->
<Objects 
	xmlns:fx="http://ns.adobe.com/mxml/2009"
	xmlns="http://www.spicefactory.org/parsley"
	xmlns:spicefactory="http://www.spicefactory.org/parsley"
	xmlns:models="models.*" 
	xmlns:controllers="controllers.*" 
	xmlns:s="library://ns.adobe.com/flex/spark" 
	>
	<fx:Script>
		<![CDATA[
			// Required for DynamicObject SWFHeaderLoader.
			import swf.*; 
		]]>
	</fx:Script>
	<fx:Declarations>
		<!--
		Parsley defined objects.
		-->
		<spicefactory:DynamicObject type = "{SWFHeaderLoader}" />
		<models:ApplicationModel/>
		<models:SWFHeaderModel/>
		<controllers:ApplicationController/>
	</fx:Declarations>
</Objects>

Air Application Descriptor File – SWFHeader-app.xml
Key here is that this an Air 2.0 project indicated on line 2 and line 8.

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<application xmlns="http://ns.adobe.com/air/application/2.0">

<!-- Adobe AIR Application Descriptor File Template.

	Specifies parameters for identifying, installing, and launching AIR applications.

	xmlns - The Adobe AIR namespace: http://ns.adobe.com/air/application/2.0
			The last segment of the namespace specifies the version 
			of the AIR runtime required for this application to run.
			
	minimumPatchLevel - The minimum patch level of the AIR runtime required to run 
			the application. Optional.
-->

	<!-- The application identifier string, unique to this application. Required. -->
	<id>SwfHeader</id>

	<!-- Used as the filename for the application. Required. -->
	<filename>SWFHeaderInspector</filename>

	<!-- The name that is displayed in the AIR application installer. 
	     May have multiple values for each language. See samples or xsd schema file. Optional. -->
	<name>SWF Header Inspector</name>

	<!-- An application version designator (such as "v1", "2.5", or "Alpha 1"). Required. -->
	<version>v0.01</version>

	<!-- Description, displayed in the AIR application installer.
	     May have multiple values for each language. See samples or xsd schema file. Optional. -->
	<!-- <description></description> -->

	<!-- Copyright information. Optional -->
	<!-- <copyright></copyright> -->

	<!-- Publisher ID. Used if you're updating an application created prior to 1.5.3 -->
	<!-- <publisherID></publisherID> -->

	<!-- Settings for the application's initial window. Required. -->
	<initialWindow>
		<!-- The main SWF or HTML file of the application. Required. -->
		<!-- Note: In Flash Builder, the SWF reference is set automatically. -->
		<content>[This value will be overwritten by Flash Builder in the output app.xml]</content>
		
		<!-- The title of the main window. Optional. -->
		<!-- <title></title> -->

		<!-- The type of system chrome to use (either "standard" or "none"). Optional. Default standard. -->
		<systemChrome>none</systemChrome>

		<!-- Whether the window is transparent. Only applicable when systemChrome is none. Optional. Default false. -->
		<transparent>true</transparent>

		<!-- Whether the window is initially visible. Optional. Default false. -->
		<!-- <visible></visible> -->

		<!-- Whether the user can minimize the window. Optional. Default true. -->
		<!-- <minimizable></minimizable> -->

		<!-- Whether the user can maximize the window. Optional. Default true. -->
		<!-- <maximizable></maximizable> -->

		<!-- Whether the user can resize the window. Optional. Default true. -->
		<!-- <resizable></resizable> -->

		<!-- The window's initial width. Optional. -->
		<!-- <width></width> -->

		<!-- The window's initial height. Optional. -->
		<!-- <height></height> -->

		<!-- The window's initial x position. Optional. -->
		<!-- <x></x> -->

		<!-- The window's initial y position. Optional. -->
		<!-- <y></y> -->

		<!-- The window's minimum size, specified as a width/height pair, such as "400 200". Optional. -->
		<!-- <minSize></minSize> -->

		<!-- The window's initial maximum size, specified as a width/height pair, such as "1600 1200". Optional. -->
		<!-- <maxSize></maxSize> -->
	</initialWindow>

	<!-- The subpath of the standard default installation location to use. Optional. -->
	<!-- <installFolder></installFolder> -->

	<!-- The subpath of the Programs menu to use. (Ignored on operating systems without a Programs menu.) Optional. -->
	<!-- <programMenuFolder></programMenuFolder> -->

	<!-- The icon the system uses for the application. For at least one resolution,
		 specify the path to a PNG file included in the AIR package. Optional. -->
	<!-- <icon>
		<image16x16></image16x16>
		<image32x32></image32x32>
		<image48x48></image48x48>
		<image128x128></image128x128>
	</icon> -->

	<!-- Whether the application handles the update when a user double-clicks an update version
	of the AIR file (true), or the default AIR application installer handles the update (false).
	Optional. Default false. -->
	<!-- <customUpdateUI></customUpdateUI> -->
	
	<!-- Whether the application can be launched when the user clicks a link in a web browser.
	Optional. Default false. -->
	<!-- <allowBrowserInvocation></allowBrowserInvocation> -->

	<!-- Listing of file types for which the application can register. Optional. -->
	<!-- <fileTypes> -->

		<!-- Defines one file type. Optional. -->
		<!-- <fileType> -->

			<!-- The name that the system displays for the registered file type. Required. -->
			<!-- <name></name> -->

			<!-- The extension to register. Required. -->
			<!-- <extension></extension> -->
			
			<!-- The description of the file type. Optional. -->
			<!-- <description></description> -->
			
			<!-- The MIME content type. -->
			<!-- <contentType></contentType> -->
			
			<!-- The icon to display for the file type. Optional. -->
			<!-- <icon>
				<image16x16></image16x16>
				<image32x32></image32x32>
				<image48x48></image48x48>
				<image128x128></image128x128>
			</icon> -->
			
		<!-- </fileType> -->
	<!-- </fileTypes> -->

</xml>

Main.mxml
The layout is a header and a body. The header is set up to be the move area for the window. he header is the top_hg HGroup on line 60. The body is the SWFHeaderBasic component on line 62. Here you could swap another body view easily.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Main view
-->
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" 
		 xmlns:s="library://ns.adobe.com/flex/spark" 
		 xmlns:mx="library://ns.adobe.com/flex/mx" 
		 xmlns:sf="http://www.spicefactory.org/parsley"
		 
		 width="100%" height="100%"
		 xmlns:views="views.*" 
		 xmlns:ui="ui.*"
		 >
	<fx:Script>
		<![CDATA[
			import ui.ApplicationCloseButtonSkin;
			/**
			 * Called once Parsley framework has reflected.
			 * */
			[Init]
			public function parsleyInit():void
			{
				top_hg.addEventListener(MouseEvent.MOUSE_DOWN, top_hg_onMouseDown);
			}
			/**
			 * Handler for top_hg MouseEvent.MOUSE_DOWN. Handles OS window move.
			 * */
			private function top_hg_onMouseDown( evt:MouseEvent):void 
			{ 
				stage.nativeWindow.startMove(); 
			}
		]]>
	</fx:Script>
	<fx:Declarations>
		<sf:Configure/>
	</fx:Declarations>
	<fx:Style>
		#appName_lbl {
			font-weight:bold;
			color: #291F65;
			font-size:18px;
		}
		#version_lbl {
			font-weight:bold;
			color: #291F65;
			font-size:10px;
		}
	</fx:Style>
	<s:VGroup width="100%" height="100%">
	<s:BorderContainer width="100%" height="100%"
					   cornerRadius="20" borderWeight="3" 
					   borderColor="0x000000" dropShadowVisible="true"
					   backgroundColor="#858282">
		<s:VGroup  verticalAlign="middle" width="100%" height="100%"
			paddingLeft="6" paddingRight="6" paddingBottom="12" 
				>
			<!--- 
			Appplication header and drag area.
			--> 			
			<s:HGroup id = "top_hg" width="100%"  verticalAlign="middle" paddingTop="12"  >
				<s:HGroup  paddingLeft="5" verticalAlign="middle" horizontalAlign="left" width="100%"  gap="30">
					<mx:Image source="@Embed(source='../assets/Adobe-swf_icon_40x40_published.png')" />
					<s:Label id = "appName_lbl" text = "SWF Header Inspection Tool"/>
				</s:HGroup>	
				<s:HGroup  paddingRight="12" verticalAlign="middle" horizontalAlign="right" width="100%"  >
					<ui:ApplicationCloseButton  
						click="NativeApplication.nativeApplication.exit()" skinClass="ui.ApplicationCloseButtonSkin"/>
					
				</s:HGroup>			
		
			</s:HGroup>
			<views:SWFHeaderBasic    width="100%" height="100%" />	
			<s:Label id = "version_lbl" text = "Version 1.00"/>
		</s:VGroup>

	</s:BorderContainer>
	</s:VGroup>	
</s:Group>

SWFHeaderBasic.mxml
Body section for the application. You can easily plug in your own view to replace this one.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Basic view.
-->
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" 
		 xmlns:s="library://ns.adobe.com/flex/spark" 
		 xmlns:sf="http://www.spicefactory.org/parsley"
		 xmlns:mx="library://ns.adobe.com/flex/mx" 
		 >
	<fx:Script>
		<![CDATA[
			import events.LoadSwfUrlHeaderRequestEvent;
			import models.SWFHeaderModel;
			import mx.binding.utils.BindingUtils;
			import mx.binding.utils.ChangeWatcher;
			import mx.events.FlexEvent;
			/**
			 * File name extracted from url or path.
			 * */
			[Bindable]
			private var fileName:String = '';
			[Inject]
			[Bindable]
			public var model:SWFHeaderModel;

			[MessageDispatcher]
			public var dispatcher:Function;
			/**
			 * Called once Parsley framework has reflected.
			 * */
			[Init]
			public function parsleyInit():void
			{
				// Bind the model changes from bytesLoaded in order to call a method.
				ChangeWatcher.watch(model, "bytesLoaded", updateProgressBar);
				addEventListener(NativeDragEvent.NATIVE_DRAG_ENTER, onDragEnterHandler);
				addEventListener(NativeDragEvent.NATIVE_DRAG_DROP, onDragDropHandler);
			}
			/**
			 * Handler for the load_btn Mouse.Click event.
			 * */
			protected function load_btn_clickHandler(event:MouseEvent):void
			{
				fileName = url_ti.text.substr(   url_ti.text.lastIndexOf( '/' ) + 1);
				dispatcher(new LoadSwfUrlHeaderRequestEvent (LoadSwfUrlHeaderRequestEvent.LOAD, url_ti.text));
			}
			/**
			 * Handler for the load_btn Mouse.Click event.
			 * */
			private function updateProgressBar(bytesLoaded:Number):void
			{
				//trace (className + ".updateProgressBar(...) - model.bytesLoaded: " + model.bytesLoaded);	
				//trace (className + ".updateProgressBar(...) - model.bytesTotal: " + model.bytesTotal);	
				bar.setProgress(model.bytesLoaded,model.bytesTotal);
				var pct:Number = Math.round((model.bytesLoaded/model.bytesTotal) * 100);
				bar.label= "Current Progress" + " " + pct + "%";
			}
			/**
			 * Handler for the NativeDragEvent.NATIVE_DRAG_ENTER event.
			 * */
			private function onDragEnterHandler(e:NativeDragEvent):void
			{
				//trace (className + ".onDragEnterHandler(...)");	
				if(e.clipboard.hasFormat(ClipboardFormats.FILE_LIST_FORMAT))
				{
					//Get the array of File objects
					var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;
					var file:File = File(files[0]);
					//Allow only one file as only supporting one in this application and soft check it is a swf file.
					if( files.length == 1 && file.url.substr(file.url.length-4).toLowerCase() == ".swf" )
					{
						//Triggers NativeDragEvent.NATIVE_DRAG_DROP event. 
						//This is when we see the drop icon.
						NativeDragManager.acceptDragDrop(this);
					}
				}
			}
			/**
			 * Handler for the NativeDragEvent.NATIVE_DRAG_DROP event.
			 * */
			private function onDragDropHandler(e:NativeDragEvent):void
			{
				//trace (className + ".onDragDropHandler(...)");	
				var files:Array = e.clipboard.getData(ClipboardFormats.FILE_LIST_FORMAT) as Array;
				var file:File = File(files[0]);
				//trace (className + ".onDragDropHandler(...) - file.url:" + file.url);
				url_ti.text = file.url;
				fileName = file.name;
				// Optional can initiate the load automatically.
				dispatcher(new LoadSwfUrlHeaderRequestEvent (LoadSwfUrlHeaderRequestEvent.LOAD, url_ti.text));
			}
		]]>
	</fx:Script>
	<fx:Declarations>
		<sf:Configure/>
		<mx:NumberFormatter id="numberFormatter" precision="0"  useThousandsSeparator="true" />
	</fx:Declarations>
	<!--- 
	Create NativeDrag target with BorderContainer
	--> 
	<s:BorderContainer 
				width="100%" height="100%"
					   borderAlpha="0"	
					   backgroundColor="0xffffff"
					   >
	<s:VGroup verticalAlign="top"  horizontalAlign="center" width="100%" height="100%"   
			   paddingTop="10"
			   >
		<s:HGroup >
			<s:Label  text="SWF URL:" paddingTop="6" />
			<s:VGroup >
				<s:HGroup  verticalAlign="middle">
				<s:TextInput id="url_ti" text="Enter swf http url or drag from desktop" width="294" />
				<s:Button  label="Load SWF" id="load_btn" click="load_btn_clickHandler(event)"/>	
				</s:HGroup>
				<!--- 
				Progress bar for slow loads such as the internet.
				--> 		
				<mx:ProgressBar id="bar"   labelPlacement="bottom" minimum="0" visible="true" maximum="100"
								color="0x323232" 
								label="Current Progress 0%" direction="right" 
								mode="manual" width="100%"/>
			</s:VGroup>
		</s:HGroup>
		<!--- 
		The result values
		--> 
		<mx:Form width="100%" height="100%"       >
			<mx:FormItem label="SWF File name: ">
				<s:Label color="0x000000" text="{fileName}"/>
			</mx:FormItem>
			<mx:FormItem label="Flash player version: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.swfVersion)}"/>
			</mx:FormItem>
			<s:HGroup>
				<mx:FormItem label="Size uncompressed: ">
					<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.sizeUncompressed)}"/>
				</mx:FormItem>
				<mx:FormItem label="Compressed: ">
					<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.sizeCompressed)}"/>
				</mx:FormItem>			
			</s:HGroup>
			<mx:FormItem label="Frame rate: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.frameRate)}"/>
			</mx:FormItem>
			<mx:FormItem label="Total frames: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.totalFrames)}"/>
			</mx:FormItem>
			<mx:FormItem label="Width: ">
				<s:Label  color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.width)}"/>
			</mx:FormItem>
			<mx:FormItem label="Height: ">
				<s:Label color="0x000000" text="{numberFormatter.format(model.swfHeader_vo.height)}"/>
			</mx:FormItem>
		</mx:Form>
	</s:VGroup>
	</s:BorderContainer>
</s:Group>

ApplicationCloseButton.as
Own version of the spark Button class.

package ui
{
	import spark.components.Button;
	/**
	 * Button to close the application
	 * */
	public class ApplicationCloseButton extends Button
	{
		public function ApplicationCloseButton()
		{
			super();
		}
	}
}

ApplicationCloseButtonSkin.mxml
Skin for the application close button class, ApplicationCloseButton. I had some trouble with the edges of the images causing a repeating state change on rollover. I solved this by making a transparent background on line 29 a few pixels larger than the bitmaps. The bitmaps then had their verticalCenter and horizontalCenter properities set to zero to keep them in the middle. See lines 34 and 36.

<?xml version="1.0" encoding="utf-8"?>
<!--- 
Skin for ui.ApplicationCloseButton
-->
<s:SparkSkin xmlns:fx="http://ns.adobe.com/mxml/2009"
			 xmlns:s="library://ns.adobe.com/flex/spark"
			 xmlns:fb="http://ns.adobe.com/flashbuilder/2009"
			 
			 alpha.disabled="0.5">
	
	<!-- host component -->
	<fx:Metadata>
		<![CDATA[
		/* @copy spark.skins.spark.ApplicationSkin#hostComponent 	*/
		[HostComponent("ui.ApplicationCloseButton")]
		]]>
	</fx:Metadata>
	
	<!-- states -->
	<s:states>
		<s:State name="up" />
		<s:State name="over" />
		<s:State name="down" />
		<s:State name="disabled" />
	</s:states>
	<!--- 
	Hit area.
	-->	
	<s:Rect  left="0" right="0" top="0" bottom="0"  width="34" height="34">
		<s:fill>
			<s:SolidColor color="0xffffff" alpha="0" />
		</s:fill>	
	</s:Rect>
	<s:BitmapImage  verticalCenter="0"  horizontalCenter="0" source="@Embed('../assets/red_glossy_close_up_button_published.png')" 
				   includeIn="up, disabled, down "/>
	<s:BitmapImage verticalCenter="0"  horizontalCenter="0"  source="@Embed('../assets/green_glossy_close_up_button_published.png')" 
				   includeIn="over"/>
	
</s:SparkSkin>

ApplicationModel.as
I could have merged the two models, but as a practice in Parsley frameworks, I create an ApplicationModel by default.

package models
{
	/**
	 * Application model.
	 * */
	public class ApplicationModel
	{

	}
}

[ad name=”Google Adsense”]
SWFHeaderModel.as
The model containing all the bound data. Flex binding is magnificent for creating model-view-controller solutions.

package models
{
	import vo.SWFHeader_vo;
	/**
	 * Model for the SWF header information.
	 * */
	public class SWFHeaderModel
	{
		/**
		 * Swf header field values.
		 * */
		[Bindable]
		public var swfHeader_vo:SWFHeader_vo;
		/**
		 * Nest the ApplicationModel.
		 * */		
		[Inject]
		[Bindable]
		public var applicationModel:ApplicationModel;
		/**
		 * Bytes loaded.
		 * */
		[Bindable]
		public var bytesLoaded:Number;
		/**
		 * Total bytes to load.
		 * */		
		[Bindable]
		public var bytesTotal:Number;
		/**
		 * Initialize model values.
		 * */		
		public function SWFHeaderModel()
		{
			swfHeader_vo = new SWFHeader_vo();
		}
	}
}

ApplicationController.as
I could have created a controller just for the SWF header if I wanted a separate model-view-controller for SWF header decoupled from the application.

package controllers
{
	import events.LoadSwfUrlHeaderRequestEvent;
	import events.SWFHeaderLoaderCompleteEvent;
	import events.SWFHeaderLoaderErrorEvent;
	import events.SWFHeaderLoaderProgressEvent;
	import flash.net.URLRequest;
	import models.SWFHeaderModel;
	import mx.controls.Alert;
	import swf.SWFHeaderLoader;
	import vo.SWFHeader_vo;
	/**
	 * Application controller
	 * */
	public class ApplicationController
	{
		/**
		 * Toggle to indicate if an Alert error is being shown. All loading errors are 
		 * routed to one method and so if there are more than one per SWFHeaderLoader.load
		 * request, this prevents multiple Alert popups.
		 * @see swfHeaderErroMessageHandler
		 * */
		private var showingSwfLoadError:Boolean = false;
		/**
		 * The SWFHeaderLoader
		 * */
		[Inject]
		public var swfHeaderLoader:SWFHeaderLoader;
		/**
		 * The model for MVC.
		 * */
		[Inject]
		[Bindable]
		public var model:SWFHeaderModel;
		/**
		 * Information Alert dialog title.
		 * */
		private var lang_info_alert_title:String = "Attention";
		/**
		 * Message when file name does not contain a .swf extension.
		 * */
		private var lang_tool_requires_swf:String = "Tool is designed for Flash Movie \".swf\" files";
		/**
		 * Message when swf file could not be loaded for any reason.
		 * */
		private var lang_unable_to_load_swf:String = "Sorry, unable to load the file.";
		/**
		 * Handler for LoadSwfUrlHeaderRequestEvent. Validate file extension. Load swf.
		 * */
		[MessageHandler]
		public function swfUrlLoadRequestMessageHandler( message : LoadSwfUrlHeaderRequestEvent ):void
		{
			model.swfHeader_vo = new SWFHeader_vo();
			// File url does not have a .swf at end.
			if ( message.swf_url.substr(message.swf_url.length-4).toLowerCase() != ".swf"  )
			{
				Alert.show(lang_tool_requires_swf ,lang_info_alert_title);
			}
			// File url has a .swf at end.
			else
			{
				showingSwfLoadError = false;
				swfHeaderLoader.load(message.swf_url);
			}
		}
		/**
		 * Handler for SWFHeaderLoaderProgressEvent.
		 * */
		[MessageHandler]
		public function swfHeaderProgressMessageHandler( message : SWFHeaderLoaderProgressEvent ):void
		{
			model.bytesTotal = message.bytesTotal;
			model.bytesLoaded = message.bytesLoaded;
		}
		/**
		 * Handler for SWFHeaderLoaderCompleteEvent.
		 * */
		[MessageHandler]
		public function swfHeaderLoadedMessageHandler( message : SWFHeaderLoaderCompleteEvent ):void
		{
			model.swfHeader_vo = message.swfHeader_vo;
		}
		/**
		 * Handler for SWFHeaderLoaderErrorEvent.
		 * */
		[MessageHandler]
		public function swfHeaderErroMessageHandler( message : SWFHeaderLoaderErrorEvent ):void
		{
			if (!showingSwfLoadError)
			{
				showingSwfLoadError = true;
				Alert.show(lang_unable_to_load_swf,lang_info_alert_title);
			}
		}		
	}
}

LoadSwfUrlHeaderRequestEvent.as
Event handler for requests to load a swf. Note how Parsley simplifies the event code.

package events
{
	import flash.events.Event;
	/**
	 * Request the loading of a swf file.
	 * */
	public class LoadSwfUrlHeaderRequestEvent extends Event
	{
		public static const LOAD:String = "event.load";
		public var swf_url:String;
		public function LoadSwfUrlHeaderRequestEvent(type:String, swf_url:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.swf_url = swf_url;
		}
	}
}

SWFHeaderLoaderProgressEvent.as
For monitoring the loading particularly when over the internet.

package events
{
	import flash.events.Event;
	/**
	 * Progress of loading a swf file.
	 * */	
	public class SWFHeaderLoaderProgressEvent extends Event
	{
		public static const PROGRESS:String = "event_SWFHeaderLoaderEvent_progress";
		/**
		 * Bytes loaded.
		 * */	
		public var bytesLoaded:Number;
		/**
		 * Total bytes to load.
		 * */		
		public var bytesTotal:Number;
		public function SWFHeaderLoaderProgressEvent(type:String, bytesLoaded:Number, bytesTotal:Number, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.bytesLoaded = bytesLoaded;
			this.bytesTotal = bytesTotal;
			
		}
	}
}

SWFHeaderLoaderCompleteEvent.as
When an SWF is completely loaded. You may want to redesign to stop once the header is loaded. However I had thought it might be nice to show the SWF at one point or proceed to extract other information.

package events
{
	import flash.events.Event;
	import vo.SWFHeader_vo;
	/**
	 * Completion of loading a swf file.
	 * */
	public class SWFHeaderLoaderCompleteEvent extends Event
	{
		public static const COMPLETE:String = "event_SWFHeaderLoaderEvent_complete";
		/**
		 * Swf file header data.
		 * */	
		public var swfHeader_vo:SWFHeader_vo;
		public function SWFHeaderLoaderCompleteEvent(type:String, swfHeader_vo:SWFHeader_vo, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
			this.swfHeader_vo = swfHeader_vo;
		}
	}
}

SWFHeaderLoaderErrorEvent.as
Errors in loading the Swf file.

package events
{
	import flash.events.Event;
	/**
	 * Errors from loading a swf file.
	 * */
	public class SWFHeaderLoaderErrorEvent extends Event
	{
		public static const SECURITY_ERROR:String = "event_SWFHeaderLoaderEvent_security";
		public static const HTTP_ERROR:String = "event_SWFHeaderLoaderEvent_HTTP";
		public static const IO_ERROR:String = "event_SWFHeaderLoaderEvent_IO";
		public function SWFHeaderLoaderErrorEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
		{
			super(type, bubbles, cancelable);
		}
	}
}
Categories
Articles

Actionscript 3 Animating Sprite Rotation Following Mouse Movement

By Lon (Alonzo) Hosford

This my own version of of Keith Peter’s Foundation Actionscript 3.0 Animation: Making Things Move chapter 3 implementation of sprite rotation following a mouse.

Download files
[August 10 2010 – I updated this to an Actionscript project in Flex Builder 4. ]

Keith Peters AS3 Animation
Learn More

You can build this with the free Flex SDK by using the code in the src folder. Same for Flash CS3 and later versions. You need to create a Flash Document in the src folder and set the document class to Chapter03_Rotation_AS3. For your convenience the Flash CS4 example download is included.

This article shows the code for the Flex project.

Application Class – Chapter03_Rotation_Flex
This Flex version is a spark implementation. The SpriteVisualElement component shown on line 51 is used to add the code to the application display on line 31.

<?xml version="1.0" encoding="utf-8"?>
<!--
	Application class for showing sprite rotation following mouse movement. 
	<p>Author: Lon Hosford https://www.lonhosford.com 908 996 3773</p>
    <p>Reference: Keith Peter's Actionscript 3.0 Animation Chapter 3</p>
	
-->
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
			   xmlns:s="library://ns.adobe.com/flex/spark" 
			   xmlns:mx="library://ns.adobe.com/flex/mx" width="400" height="400"
			   creationComplete="creationCompleteHandler(event)" 
			   viewSourceURL="srcview/index.html">
	<fx:Script>
		<![CDATA[
			import mx.events.FlexEvent;
			
			// Properties for background
			private static const backgroundColor:Number = 0x0000ff;
			private static const backgroundBorderColor:Number = 0x666666;
			private static const backgroundBorderWeight:Number = 2;
			/**
			 * Handler for Application creationComplete event
			 * */
			protected function creationCompleteHandler(event:FlexEvent):void
			{
				// Create an Arrow object
				var arrow:Arrow = new Arrow();
				// Wrap arrow object into a RotateSpriteToMouse object
				var rotateToMouse:RotateSpriteToMouse = new RotateSpriteToMouse(arrow);
				// Add rotateToMouse Sprite to a SpriteVisualElement
				arrowVisualElement.addChild(rotateToMouse);
				// Center the SpriteVisualElement
				arrowVisualElement.x = background_bc.width / 2;
				arrowVisualElement.y = background_bc.height / 2;
			}

		]]>
	</fx:Script>
	<!--- 
	Background for app 
	--> 
	<s:BorderContainer id = "background_bc"
					   width="{width}" height = "{height}"
					   borderWeight="{backgroundBorderWeight}"
					   borderColor="{backgroundBorderColor}"
					   backgroundColor="{backgroundColor}">

		<!--- 
		Spark container for Sprite 
		--> 
		<s:SpriteVisualElement id="arrowVisualElement" />

	</s:BorderContainer>
					   
</s:Application>

[ad name=”Google Adsense”]
RotateSpriteToMouse Class
This is the class that does the work. Listening to the Event.ENTER_FRAME event leads to the update_rotation() method that does the work of updating the rotation.

package
{
	import flash.display.Sprite;
	import flash.events.Event;
	/**
	 * Rotates sprite to mouse position
	 * */
	public class RotateSpriteToMouse extends Sprite
	{
		private var _sprite_to_rotate:Sprite;	// Sprite to rotate to mouse
		/**
		 * Constructor 
		 * @param sprite_to_rotate The sprite to rotate
		 * */
		public function RotateSpriteToMouse(sprite_to_rotate:Sprite)
		{
			_sprite_to_rotate = sprite_to_rotate;
			addChild(_sprite_to_rotate);
			addEventListener(Event.ENTER_FRAME, enterFrameEventHandler);
		}
		/**
		 * The event handler for Event.ENTER_FRAME
		 * */
		private function enterFrameEventHandler(event:Event):void
		{
			update_rotation();
		}
		/**
		 * Updates the rotation of the _sprite_to_rotate
		 * */
		private function update_rotation():void
		{
			// Triangle adjacent angle side distance for the x value.
			var dx:Number = mouseX - _sprite_to_rotate.x;
			// Triangle opposite angle side distance for the y value.
			var dy:Number = mouseY - _sprite_to_rotate.y;
			// Compute angle in radians from the sprite to the mouse position.
			var radians:Number = Math.atan2(dy, dx);
			// Convert radians to degrees
			_sprite_to_rotate.rotation = radians * 180 / Math.PI;
		}
	}
}

Arrow Class
Simple arrow sprite that Keith wrote. Key here is the center registration point.

package
{
	import flash.display.Sprite;
	/**
	 * Creates an arrow sprite with fixed dimensions
	 * */
	public class Arrow extends Sprite
	{
		public function Arrow() 
		{
			draw();
		}
		/**
		 * Draw the arrow
		 * */
		private function draw():void
		{
			graphics.lineStyle(1, 0, 1);
			graphics.beginFill(0xffff00);
			graphics.moveTo(-50, -25);
			graphics.lineTo(0, -25);
			graphics.lineTo(0, -50);
			graphics.lineTo(50, 0);
			graphics.lineTo(0, 50);
			graphics.lineTo(0, 25);
			graphics.lineTo(-50, 25);
			graphics.lineTo(-50, -25);
			graphics.endFill();
		}
	}
	
}