Adobe, MAKE SOME NOISE

Today - as is usually the case with these posts - I found myself writing code that I've written a dozen times before. I had some animation keyframed out on the timeline that I wanted to export as a stack of pngs.

I know there are a few tools built in that should be able to do this for me, but in the past I've had lots of trouble getting them to work - I usually end up getting dropped frames and strange behavior for nested MCs and the like.

So today I took a little extra time and made a timeline exporter that should be pretty robust. The basic idea is that we take manual control of the framerate and snapshot each frame, which is saved out to a folder during runtime. In order to pull this off, we need to the flash.filesystem package so your fla needs to be targetting the AIR runtime. In CS5.5 you can change this on the fly under Publish Settings (look for the Player menu up at the top) - I recall that on older versions of Flash you would have to make a new AIR project and copy your animation into it. FWIW it wouldn't be too hard to modify this script to load an external swf file and use that as the animation source if copying things out of one fla into another is too big of a problem.

Anyhow - with your animation in a fla targetting AIR - drop this code into the first frame of the timeline and jump into the configuration block at the top of the script. You'll want to set up your cropping and file path here, and set targetMC to point at whatever you want to capture; it can be a specific MovieClip or you can just set it to "this" and grab the whole stage.

When you have the config options fine tuned, compile the fla (you don't even need to save it) and you'll be prompted for a directory to save into. As soon as you do that, the animation is captured frame by frame and saved off into the folder you chose.

Two things to be aware of in here - first, this script uses recursion in a couple of places. If you're not familiar (or even if you are) that means that the amount of processing going on is going to scale up with how many MCs and other objects the code has to dig through. Anything with a ton of symbols or very deep nesting could get you into trouble. I highly recommend you save your work (in all programs - not just Flash) before testing this! It is very possible to crash the player as well as Flash or even your whole system. You've been warned - use this at your own risk.

Second - the png exporting used here is from as3corelib. I copied the com.adobe.images.PNGEncoder class right into the bottom of the script (with some tiny tweaks) to make the whole thing self-contained. While everything I write for this blog is available under the MIT license unless otherwise noted, the portion of this script from as3corelib is offered under their original license included at the top of that block of code, and also available on the as3 corelib project page here. If you encounter Darron Schall or Mike Chambers - buy those guys a beer or two!

  1. //====================================================
  2. // WARNING
  3. //====================================================
  4.  
  5. /**
  6. * WARNING: This script uses recursion.
  7. * For animations with a large number of symbols
  8. * or deep nesting, this may run slowly and
  9. * could time out the script.
  10. *
  11. * This script could also crash itself, Flash,
  12. * or your entire computer - please save your work
  13. * before testing. Use at your own risk!
  14. */
  15.  
  16. //====================================================
  17. // CONFIGURATION
  18. //====================================================
  19.  
  20. // REMEMBER! If you're getting errors on compile, make sure
  21. // the project is set up to export as an AIR app - not a swf!
  22.  
  23. // name of the file - {FRAME_NUMBER} will be populated dynamically
  24. var outputFileName:String = 'demo_{FRAME_NUMBER}.png';
  25.  
  26. // fill in the instance name of your animation
  27. var targetMC:MovieClip = this;
  28.  
  29. // dimensions of the images saved out
  30. var outputWidth:int = 79;
  31. var outputHeight:int = 79;
  32.  
  33. // do you want transparent-pngs or not?
  34. var transparent:Boolean = true;
  35.  
  36. // if not-transparent - what color background?
  37. var matteColor:uint = 0x000000;
  38.  
  39. // Number of characters in the frame number. Ex. 3 = "001", 5 = "00001"
  40. var frameNumberDigits:uint = 3;
  41.  
  42. // Whether we should look for nested animations.
  43. // This may be very cpu intensive!
  44. // NOTE: Newer versions of Flash do this automatically.
  45. var useNestedAnimation:Boolean = true;
  46.  
  47. // Whether nested animations will loop or stop when finished playing.
  48. var loopNestedAnimations:Boolean = true;
  49.  
  50. //====================================================
  51. // DO NOT EDIT BELOW THIS POINT
  52. //====================================================
  53.  
  54. import flash.filesystem.*;
  55. import flash.display.*;
  56.  
  57. // stop playback - we'll control this on our own from here on out
  58. stop();
  59. (useNestedAnimation) ? stopRecursively(targetMC) : targetMC.stop();
  60.  
  61. // a few vars we'll need over the frame loop
  62. var bmd:BitmapData;
  63. var baseFile:File = new File();
  64. var fs:FileStream = new FileStream();
  65.  
  66. // ask for a directory we can save into
  67. baseFile.addEventListener(Event.SELECT, saveToDirectoryChosen);
  68. baseFile.addEventListener(Event.CANCEL, saveToDirectoryAborted);
  69. baseFile.browseForDirectory('Save image stack to');
  70.  
  71. function saveToDirectoryChosen(e:Event):void{
  72. e.target.removeEventListener(Event.SELECT, saveToDirectoryChosen);
  73. e.target.removeEventListener(Event.CANCEL, saveToDirectoryAborted);
  74. addEventListener('enterFrame', eFrame);
  75. }
  76.  
  77. function saveToDirectoryAborted(e:Event):void{
  78. e.target.removeEventListener(Event.SELECT, saveToDirectoryChosen);
  79. e.target.removeEventListener(Event.CANCEL, saveToDirectoryAborted);
  80. trace('Save directory not chosen - aborting export.');
  81. }
  82.  
  83. function eFrame(e:Event):void{
  84. bmd = new BitmapData(outputWidth, outputHeight, transparent, matteColor);
  85. bmd.draw(targetMC);
  86.  
  87. var num:String = String(targetMC.currentFrame);
  88. while(num.length < frameNumberDigits) num = '0' + num;
  89.  
  90. var f:File = new File(baseFile.url + '/' + outputFileName.replace('{FRAME_NUMBER}', num));
  91. trace('Frame ' + num + '\t=>\t' + f.url);
  92.  
  93. fs.open(f, 'write');
  94. fs.writeBytes(encode(bmd));
  95. fs.close();
  96.  
  97. if(targetMC.currentFrame == targetMC.totalFrames){
  98. e.target.removeEventListener(e.type, arguments.callee);
  99. trace('========================================');
  100. trace(targetMC.totalFrames + ' frames captured over ' + (getTimer() / 1000).toFixed(2) + 'seconds.');
  101. } else {
  102. advanceFrame(targetMC as DisplayObjectContainer);
  103. }
  104. }
  105.  
  106. // WARNING: This is a recursive function - it may bog down or crash on
  107. // excessively deep or complicated display trees!
  108. // SAVE YOUR FILE BEFORE TESTING!!
  109. function advanceFrame(doc:DisplayObjectContainer):void{
  110. if(!doc || doc.numChildren < 1) return;
  111.  
  112. // loop through children - recursively advancing each mc we find
  113. var i:int = doc.numChildren;
  114. while(i--){
  115. var dobj:DisplayObject = doc.getChildAt(i);
  116. if(dobj is DisplayObjectContainer) advanceFrame(dobj as DisplayObjectContainer);
  117. }
  118.  
  119. // then nextframe self
  120. if(doc is MovieClip && MovieClip(doc).totalFrames > 1){
  121. if(MovieClip(doc).currentFrame == MovieClip(doc).totalFrames){
  122. if(loopNestedAnimations) MovieClip(doc).gotoAndStop(1);
  123. } else {
  124. MovieClip(doc).nextFrame();
  125. }
  126. }
  127. }
  128.  
  129. // WARNING: This is a recursive function - it may bog down or crash on
  130. // excessively deep or complicated display trees!
  131. // SAVE YOUR FILE BEFORE TESTING!!
  132. function stopRecursively(doc:DisplayObjectContainer):void{
  133. if(!doc) return;
  134. if(doc is MovieClip) MovieClip(doc).stop();
  135.  
  136. var i:int = doc.numChildren;
  137. while(i--){
  138. var dobj:DisplayObject = doc.getChildAt(i);
  139. if(dobj is DisplayObjectContainer) stopRecursively(dobj as DisplayObjectContainer);
  140. }
  141. }
  142.  
  143. //====================================================
  144. //====================================================
  145. // Below here is the PNGEncoder Class from Adobe's
  146. // as3corelib - embedded directly here for so you
  147. // can quickly paste this into a .fla without
  148. // setting up import paths or project structure.
  149. //
  150. // Class and scope structure commented out,
  151. // no changes made to functionality.
  152. //
  153. // @see: https://github.com/mikechambers/as3corelib
  154. //====================================================
  155. //====================================================
  156.  
  157. /*
  158.   Copyright (c) 2008, Adobe Systems Incorporated
  159.   All rights reserved.
  160.  
  161.   Redistribution and use in source and binary forms, with or without
  162.   modification, are permitted provided that the following conditions are
  163.   met:
  164.  
  165.   * Redistributions of source code must retain the above copyright notice,
  166.   this list of conditions and the following disclaimer.
  167.  
  168.   * Redistributions in binary form must reproduce the above copyright
  169.   notice, this list of conditions and the following disclaimer in the
  170.   documentation and/or other materials provided with the distribution.
  171.  
  172.   * Neither the name of Adobe Systems Incorporated nor the names of its
  173.   contributors may be used to endorse or promote products derived from
  174.   this software without specific prior written permission.
  175.  
  176.   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  177.   IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  178.   THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  179.   PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  180.   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  181.   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  182.   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  183.   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  184.   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  185.   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  186.   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  187. */
  188. //package com.adobe.images
  189. //{
  190. import flash.geom.*;
  191. import flash.display.Bitmap;
  192. import flash.display.BitmapData;
  193. import flash.utils.ByteArray;
  194.  
  195. /**
  196. * Class that converts BitmapData into a valid PNG
  197. */
  198. // public class PNGEncoder
  199. // {
  200. /**
  201. * Created a PNG image from the specified BitmapData
  202. *
  203. * @param image The BitmapData that will be converted into the PNG format.
  204. * @return a ByteArray representing the PNG encoded image data.
  205. * @langversion ActionScript 3.0
  206. * @playerversion Flash 9.0
  207. * @tiptext
  208. */
  209. /*public static*/ function encode(img:BitmapData):ByteArray {
  210. // Create output byte array
  211. var png:ByteArray = new ByteArray();
  212. // Write PNG signature
  213. png.writeUnsignedInt(0x89504e47);
  214. png.writeUnsignedInt(0x0D0A1A0A);
  215. // Build IHDR chunk
  216. var IHDR:ByteArray = new ByteArray();
  217. IHDR.writeInt(img.width);
  218. IHDR.writeInt(img.height);
  219. IHDR.writeUnsignedInt(0x08060000); // 32bit RGBA
  220. IHDR.writeByte(0);
  221. writeChunk(png,0x49484452,IHDR);
  222. // Build IDAT chunk
  223. var IDAT:ByteArray= new ByteArray();
  224. for(var i:int=0;i < img.height;i++) {
  225. // no filter
  226. IDAT.writeByte(0);
  227. var p:uint;
  228. var j:int;
  229. if ( !img.transparent ) {
  230. for(j=0;j < img.width;j++) {
  231. p = img.getPixel(j,i);
  232. IDAT.writeUnsignedInt(
  233. uint(((p&0xFFFFFF) << 8)|0xFF));
  234. }
  235. } else {
  236. for(j=0;j < img.width;j++) {
  237. p = img.getPixel32(j,i);
  238. IDAT.writeUnsignedInt(
  239. uint(((p&0xFFFFFF) << 8)|
  240. (p>>>24)));
  241. }
  242. }
  243. }
  244. IDAT.compress();
  245. writeChunk(png,0x49444154,IDAT);
  246. // Build IEND chunk
  247. writeChunk(png,0x49454E44,null);
  248. // return PNG
  249. return png;
  250. }
  251.  
  252. /*private static*/ var crcTable:Array;
  253. /*private static*/ var crcTableComputed:Boolean = false;
  254.  
  255. /*private static*/ function writeChunk(png:ByteArray,
  256. type:uint, data:ByteArray):void {
  257. if (!crcTableComputed) {
  258. crcTableComputed = true;
  259. crcTable = [];
  260. var c:uint;
  261. for (var n:uint = 0; n < 256; n++) {
  262. c = n;
  263. for (var k:uint = 0; k < 8; k++) {
  264. if (c & 1) {
  265. c = uint(uint(0xedb88320) ^
  266. uint(c >>> 1));
  267. } else {
  268. c = uint(c >>> 1);
  269. }
  270. }
  271. crcTable[n] = c;
  272. }
  273. }
  274. var len:uint = 0;
  275. if (data != null) {
  276. len = data.length;
  277. }
  278. png.writeUnsignedInt(len);
  279. var p:uint = png.position;
  280. png.writeUnsignedInt(type);
  281. if ( data != null ) {
  282. png.writeBytes(data);
  283. }
  284. var e:uint = png.position;
  285. png.position = p;
  286. c = 0xffffffff;
  287. for (var i:int = 0; i < (e-p); i++) {
  288. c = uint(crcTable[
  289. (c ^ png.readUnsignedByte()) &
  290. uint(0xff)] ^ uint(c >>> 8));
  291. }
  292. c = uint(c^uint(0xffffffff));
  293. png.position = e;
  294. png.writeUnsignedInt(c);
  295. }
  296. // }
  297. //}

Stringify Anything

December 5th, 2011

This probably brands me as an old-school AS2 crank, but I have to admit that I'm not the biggest fan of the FlashBuilder debugger. Don't get me wrong, I use it daily and it's miles ahead of the Flash Pro version; but for my money, there is still a lot of mileage left in the old trace console.

To really get good utility out of tracing, I find that I write the same helper methods over and over again in every project. The big one being a function for converting collections (Objects, Arrays, etc) to a readable format. I took a little time over the last week and tried to get this implementation as bulletproof as possible, and I'm finally pretty happy with where it is.

I'm intending to add this to T-Rex Arms either as a top level utility function, or else as a helper in the Console. In the meantime, you can drop it into a project wherever you need an eye on your data.

  1. /**
  2.  * Attempts to convert any object to a human-readable string format.
  3.  * Ordered collections (Arrays and Vectors) are listed in Array notation: [1, 2, 3]
  4.  * Associative arrays are listed in Object notation between square braces: ["a":1, "b":2, "c":3]
  5.  * Unordered collections (Objects and Dictionaries) are listed in Object notation: {a:1, b:2, c:3}
  6.  *
  7.  * This function will use recursion to deep-trace nested objects. For
  8.  * better control of this, there is a maxLevelsDeep parameter that will
  9.  * limit the number of levels deep the trace can go. A value of 0 allows
  10.  * no nesting (ex: {a:[object Object]}), a positive value specifies the
  11.  * number of layers down permitted, and a negative value will recurse
  12.  * indefinitely.
  13.  *
  14.  * WARNING: attempting to stringify large or deep objects
  15.  * can create timeouts. Using extremely deep objects or objects with circular
  16.  * references can trigger stack overflow errors. This utility should only be
  17.  * used during development - not on production code!
  18.  *
  19.  * @param data Any Object, collection, or piece of data to conver to a String.
  20.  * @param maxLevelsDeep The maximum recursion allowed. Default is 3 layers deep.
  21.  * @return A string representation of the data passed in.
  22. */
  23. function stringify(data:*, maxLevelsDeep:int = 3):String{
  24. if(data == null) return 'null';
  25. if(data == undefined) return 'undefined';
  26. if(data is String) return '"' + data + '"';
  27. if(data is Boolean) return (data) ? 'true' : 'false';
  28. if(data is Number) return Number(data).toString(); // this will catch int and uint as well
  29.  
  30. // if this condition triggers we're dealing with something not listed above but not an object...mystery primitive?
  31. if(!data || (!(data is Object) && !'constructor' in data)) return data + '';
  32.  
  33. var s:String = '', i:int = 0, len:int = 0, alen:int = 0, key:*, val:*, o:Object, a:Array, v:Vector.<*>, isAssociative:Boolean, toStr:String;
  34.  
  35. if(data is Array){
  36. a = data as Array;
  37. len = a.length;
  38. s = '[';
  39.  
  40. // to detect associative arrays, we look for any keys that are not
  41. // type int, and we look for more elements than there are indices
  42. for(key in a){
  43. if(!(key is int) || ++alen > len){
  44. isAssociative = true;
  45. break;
  46. }
  47. }
  48.  
  49. if(isAssociative){
  50. // confirmed that we are dealing with an associative array...
  51. // we need to parse this by key
  52. for(key in a){
  53. val = a[key];
  54. s += (key is String) ? '"' + key + '":' : key + ':';
  55.  
  56. // this block of checks is more verbose but it guards us against a lot of unnecessary recursion...
  57. if(val == null) s += ', ';
  58. else if(val == undefined) s += 'undefined, ';
  59. else if(val is String) s += '"' + val + '", ';
  60. else if(val is Boolean) s += (data) ? 'true, ' : 'false, ';
  61. else if(val is Number) s += Number(val).toString() + ', ';
  62. else if(maxLevelsDeep) s += stringify(val, maxLevelsDeep - 1) + ', ';
  63. else if(val is Array || val is Vector.<*> || val is Vector.<int> || val is Vector.<uint> || val is Vector.<Number>) s += '[' + val + '], ';
  64. else s += val + ', ';
  65. }
  66.  
  67. } else {
  68. // this is a normal array - we can parse by index
  69. if(!len) return '[]';
  70. for(i = 0; i < len; ++i){
  71. val = a[i];
  72. if(val == null) s += ', ';
  73. else if(val == undefined) s += 'undefined, ';
  74. else if(val is String) s += '"' + val + '", ';
  75. else if(val is Boolean) s += (data) ? 'true, ' : 'false, ';
  76. else if(val is Number) s += Number(val).toString() + ', ';
  77. else if(maxLevelsDeep) s += stringify(val, maxLevelsDeep - 1) + ', ';
  78. else if(val is Array || val is Vector.<*> || val is Vector.<int> || val is Vector.<uint> || val is Vector.<Number>) s += '[' + val + '], ';
  79. else s += val + ', ';
  80. }
  81. }
  82.  
  83. if(s.length < 3) return '[]';
  84. if(s.substr(s.length - 2, 2) == ', ') s = s.substring(0, s.length - 2);
  85. return s + ']';
  86. }
  87.  
  88. if(data is Vector.<*> || data is Vector.<int> || data is Vector.<uint> || data is Vector.<Number>){
  89. // The number types (Number, int, uint) don't seem to upcast to
  90. // work as Vector.<*> so we need some extra cases here...
  91. s = '[';
  92. v = data;
  93. len = v.length;
  94. for(i = 0; i < len; ++i){
  95. val = v[i];
  96. if(val == null) s += ', ';
  97. else if(val == undefined) s += 'undefined, ';
  98. else if(val is String) s += '"' + val + '", ';
  99. else if(val is Boolean) s += (data) ? 'true, ' : 'false, ';
  100. else if(val is Number) s += Number(val).toString() + ', ';
  101. else if(maxLevelsDeep) s += stringify(val, maxLevelsDeep - 1) + ', ';
  102. else if(val is Array || val is Vector.<*> || val is Vector.<int> || val is Vector.<uint> || val is Vector.<Number>) s += '[' + val + '], ';
  103. else s += val + ', ';
  104. }
  105.  
  106. if(s.length < 3) return '[]';
  107. if(s.substr(s.length - 2, 2) == ', ') return s.substring(0, s.length - 2) + ']';
  108. return s + ']';
  109. }
  110.  
  111. // At this point we're dealing with either a complex Object...
  112. s = '{';
  113. o = data as Object;
  114.  
  115. for(key in o){
  116. s += key + ':';
  117. val = o[key];
  118. if(val == null) s += 'null, ';
  119. else if(val == undefined) s += 'undefined, ';
  120. else if(val is String) s += '"' + val + '", ';
  121. else if(val is Boolean) s += (data) ? 'true, ' : 'false, ';
  122. else if(val is Number) s += Number(val).toString() + ', ';
  123. else if(maxLevelsDeep) s += stringify(val, maxLevelsDeep - 1) + ', ';
  124. else if(val is Array || val is Vector.<*> || val is Vector.<int> || val is Vector.<uint> || val is Vector.<Number>) s += '[' + val + '], ';
  125. else s += val + ', ';
  126. }
  127.  
  128. if(s.length < 3){
  129. if(Object(o).constructor && Object(o).constructor != Object){
  130. if('toString' in o && o['toString'] && o['toString'] is Function){
  131. try{
  132. // we only attempt this on complex data types with a
  133. // defined toString method that didn't stringify above...
  134. toStr = o.toString();
  135. if(toStr.length > 2) return toStr;
  136. } catch(err:Error){}
  137. }
  138.  
  139. // no toString() - no visible attributes, and not an Object
  140. // lets just send back the class...
  141. return Object(o).constructor + '';
  142. }
  143.  
  144. // This should only trigger for raw Objects with nothing in them...
  145. return '{}';
  146. }
  147.  
  148. if(s.substr(s.length - 2, 2) == ', ') return s.substring(0, s.length - 2) + '}';
  149. return s + '}';
  150. }

I set it up to handle all of the collection types: Arrays, Associative Arrays, Vectors, Dictionaries, and Objects, in addition to the primitive data types. If the input doesn't fall into those categories, it will still try to read any public attributes visible to a for-in loop.

This function is also set up for deep recursion (read the header comment) for nested or multi-dimensional collections. I hope someone else out there gets some use from this - and as always, if you find any bugs please let me know. Finally, if you want to explicitly add support to a class, just give it a toString() method. And now for some examples!

  1. var someObj:Object = {a:1, b:2, c:3};
  2. trace(stringify(someObj));
  3. // {a:1, b:2, c:3}

  1. var someObj:Dictionary = new Dictionary();
  2. someObj[stage] = new Date();
  3. someObj['foo'] = 'bar';
  4. trace(stringify(someObj));
  5. // {[object Stage]:Mon Dec 5 15:43:59 GMT-0800 2011, foo:"bar"}

  1. var someObj:Vector.<BitmapData> = new Vector.<BitmapData>();
  2. someObj.push(new BitmapData(1, 1));
  3. someObj.push(new BitmapData(2, 2));
  4. someObj.push(new BitmapData(3, 3));
  5. trace(stringify(someObj));
  6. // [[object BitmapData], [object BitmapData], [object BitmapData]]

  1. var someObj:Array = [0, 1, 2];
  2. someObj[5] = 'five';
  3. trace(stringify(someObj));
  4. // [0, 1, 2, , , "five"]

  1. var someObj:Array = [];
  2. someObj['foo'] = {a:1, b:2};
  3. someObj['bar'] = {c:3, d:4};
  4. trace(stringify(someObj));
  5. // ["bar":{d:4, c:3}, "foo":{a:1, b:2}]

Data Sanitization

December 4th, 2011

In the past few years I've worked with engineers from a lot of different backgrounds, which has really helped me define my own coding philosophy. It's also taught me to sanitize my data at every possible point of failure. In development this has saved me countless hours of trying to track down little bits of weirdness - I can't count the number of times a little data checking has removed a weird bug symptom on a production app when I didn't have the time to dig into deep systems and track down the original source of the bug.

This is obviously a subject as broad as there are devs in the world so I'm not going to claim this is foolproof for every situation - I'll just say that it's my go-to method for catching a lot of bugs as they get written, before they can crash the application.

  1. // A function expecting a generic Object with
  2. // "key" and "value" attributes.
  3. // ex. {key:'foo', value:'bar'}
  4. public function someFunc(o:Object):void{
  5. if(!o || !(o is Object)){
  6. Console.error('Received invalid argument, expected [object Object], received ' + o);
  7. return;
  8. }
  9.  
  10. if(!'key' in o || !'value' in o){
  11. Console.error('Malformed Object received: ' + stringify(o));
  12. return;
  13. }
  14.  
  15. if(!o.key || !(o.key is String) || !o.value || !(o.value is String)){
  16. Console.warn('Empty parameters encountered: ' + stringify(o));
  17. }
  18.  
  19. // here, we know we have a valid object to process
  20. }

A couple of things: I use the Console class from T-Rex Arms all the time now for debugging - in this example, those calls can be replaced with traces. Second, I use a stringify function to convert complex data types to readable strings - I'm polishing that function and I should post that code in the nearish future; until then you can just breakpoint those lines when they get executed.

The actual nuts and bolts of this are happening in the if-then conditions. The first checks that we have an argument at all. This will catch null, undefined, 0, NaN, false Booleans, and empty strings. This is always my first line of defense because it protects against null pointer errors later in the function. The second half of that condition is making sure we're at least dealing with an Object in some capacity - no primitive types.

The next if-then is specific to checking Objects - this is where things will diverge pretty quickly for different types of data. I deal with a lot of JSON objects coming from a server or through javascript so I always make sure my attributes exist before I start calling them.

Lastly I like to verify that the data in the attributes exist and is what I'm expecting. This is another spot where things will get pretty detailed as the data gets more complex.

If this were coming from javascript (read: Unfucking the ExternalInterface) I will use the rest operator to allow any possible value(s) to come in, before I start sanitizing. This ensures that even if a malicious call is injected, I can catch it and fail gracefully instead of locking up the application.

  1. public function someFuncCalledByJavaScript(...args):Boolean{
  2. if(!args || args.length < 1){
  3. Console.error('JS call received with no data: expecting 1 arg');
  4. return false;
  5. }
  6.  
  7. if(!args[0] || !(args[0] is Object)){
  8. Console.error('JS call received invalid argument, expected [object Object], received ' + o);
  9. return false;
  10. }
  11.  
  12. var o:Object = args[0] as Object;
  13.  
  14. // now check the expected keys and values as before...
  15.  
  16. return true; // let JS know it did good.
  17. }

So that's the basic checking I use. And here are some shorthands I use when I trust the data source a little better but just want to make sure things are kosher before processing them.

  1. public function someStringFunc(s:String):void{
  2. if(s && s.length) // do stuff
  3. }
  4.  
  5. public function someNumFunc(n:Number):void{
  6. if(n && !isNaN(n)) // do stuff
  7. }
  8.  
  9. public function someIntFunc(i:int):void{
  10. if(i && !isNaN(i) && i > 0) // do stuff
  11. }

FoamPlucker for iPad

September 30th, 2011

I pushed my iPad build of FoamPlucker.com to the app store on Sunday evening and it got approved last night. If you don't use Sabol foam trays to store small, oddly shaped things, then it's probably not a useful app for you - but as far as developing AS3 for iOS deployment - I learned a lot.

FoamPlucker.com

On the app store

I always jump at the chance to write a utility in AIR to help other members of my team. The short turnaround and direct communication between engineer and consumer really reminds me how hard it is to see software from the end user's perspective, and how much a small feature or bug can make or break someone's day.

One little feature I really like to include is the Application Updater built into AIR. I just implemented this a couple days ago so I wanted to document it while the pitfalls are fresh in my mind.

Start out by going into your application XML (usually AppName.xml in the default package) and find the version number. In AIR 2.5 and later this tag is <versionNumber>, before that it was just <version>; you can find the schema you're using at the end of the url in the second line of the file - something like this:

  1. <?xml version="1.0" encoding="utf-8" standalone="no"?>
  2. <application xmlns="http://ns.adobe.com/air/application/2.6">
  3.  
  4. <!-- Adobe AIR Application Descriptor File Template.
  5. ...
  6. -->
  7.  
  8. <versionNumber>1.2</versionNumber>
  9.  

For the updater to work, the installed versions of the app will look for an xml manifest (somewhere on your webserver) to compare its own version to. Here is an example of what that file looks like:

  1. <!--?xml version="1.0" encoding="utf-8"?-->
  2. <update xmlns="http://ns.adobe.com/air/framework/update/description/2.5">
  3.  
  4. <versionNumber>1.2</versionNumber>
  5. <url>http://www.static.path.to/my/Application.air</url>
  6.  
  7. <description>
  8. 1.2
  9. Bug fixes in the UI.
  10. Fixed intermittent NPE on startup.
  11.  
  12. 1.1
  13. Optimized save/load performance.
  14. Reduced CPU load while rendering.
  15.  
  16. 1.0
  17. First version of the application - core functionality.
  18. </description>
  19.  
  20. </update>

Again, if you're on AIR 2.5 or later, use the 2.5 schema and the <versionNumber> tag, if you're on an earlier SDK, use the 1.0 schema and the <version> tag. The url in here is a direct link to the updated .air file, and the description text will show up as "Release notes" during the upgrade. Save this file out to a webserver somewhere and copy the address for the next step.

WARNING: As soon as you upload this file, people will start getting prompted to update - make sure the new app is ready!

Now that the xml is set up - you can handle the rest in ActionScript. As soon as your application starts up, you'll want to call this code:

  1. var updater:ApplicationUpdaterUI = new ApplicationUpdaterUI();
  2.  
  3. // point to the xml manifest on the server
  4. updater.updateURL = "http://www.static.path.to/my/update_descriptor.xml"
  5.  
  6. updater.isCheckForUpdateVisible = false;
  7. updater.isDownloadUpdateVisible = true;
  8. updater.isDownloadProgressVisible = true;
  9. updater.isInstallUpdateVisible = true;
  10.  
  11. updater.addEventListener(UpdateEvent.INITIALIZED, function(e:UpdateEvent):void{
  12. e.target.removeEventListener(e.type, arguments.callee);
  13. updater.checkNow();
  14. });
  15.  
  16. updater.initialize();

Export a release build and upload the finished file to where the update descriptor is expecting it and you're finished. The next time you increment the version number in the manifest and app.xml, your users will be prompted to upgrade the next time they start the app.