Dan's Tech Den For the Cyber Warrior

3Nov/120

Embed Images as binaryData in AS3 to reduce SWF size

This evolved from a lengthy discussion on the file size of an SWF.

It seems that Flash/Flex embeds PNGs as 32bit only. And this in some cases causes the size of the image to be greatly increased. In some cases the size of the image more than doubles if the image had been compressed using optimization utilities.

The solution to keep the reduced size that I came up with is to embed the image as binaryData instead of a bitmap using the mimeType='application/octet-stream' like this

[Embed(source = "assets/8bit.png", mimeType = "application/octet-stream")]
public static const IMG:Class;

But this gives us a ByteArray instead of the needed bitmap.

This ByteArray can be decoded into a bitmap using the Loader.loadBytes() function, but this function is asynchronous and thus this step needs to be performed in advance, during initial game loading.

For this I have made the following OctetStreamBitmapDecoder class

/**
 * An asynchronous bitmap data decoder, for assets embedded as octet streams
 * 
 * @author Danish Goel
 * @copyright 2012 Danish Goel
 * @license MIT License
 */
package com.danishgoel 
{
    import flash.display.Bitmap;
    import flash.display.Loader;
    import flash.events.Event;
    import flash.utils.ByteArray;
    import flash.utils.describeType;
 
    /**
     * ...
     * @author Danish Goel
     */
    public class OctetStreamBitmapDecoder
    {
        // image loaded vs total
        public var _totalImages:uint;
        public var _loadedImages:uint = 0;
 
        // matching regex for assets
        private var _regex:RegExp;
 
        // call on loading complete
        private var _onComplete:Function;
 
        // call on each image decode
        private var _onProgress:Function;
 
        /**
         * Initialize the asset loader
         * 
         * @param    assetsClass       The class which contains the embedded octet-stream bitmaps
         * @param    onLoadComplete    [optional] function to call once all the images are decoded
         * @param    matchRegex        [optional] only decode constants matching the given regex
         * @param    onProgress        [optional] call on each image decode with percent complete
         */
        public function OctetStreamBitmapDecoder(assetsClass:Class, onComplete:Function = null, matchRegex:RegExp = null, onProgress:Function = null) {
            // assign parameters
            _regex = matchRegex;
            _onComplete = onComplete;
            _onProgress = onProgress;
 
            // get type info (reflection)
            var assetsXML:XML = describeType(assetsClass);
 
            // total number of images
            _totalImages = assetsXML.constant.length();
 
            // loop over all constants
            for each(var item:XML in assetsXML.constant) {
                var notDecodable:Boolean = true;
 
                // if item is of type Class and matches the given regex (if any)
                if (item.@type == "Class" && (_regex == null || _regex.test(item.@name))) {
                    // if its also a ByteArray
                    if (new assetsClass[item.@name] is ByteArray) {
                        // load the image using the byteArray
                        loadImage(assetsClass[item.@name]);
 
                        // and it is decodable
                        notDecodable = false;
                    }
                }
 
                // if the constant is not decodable, remove it from total count
                if (notDecodable) _totalImages--;
            }
        }
 
        /**
         * Asynchronously decode the byteStream image to a bitmapData
         * @param    byteSrc        Image byte stream
         */
        public function loadImage(byteSrc:Class):void {
            // loader to load the image
            var loader:Loader = new Loader();
 
            // on load complete
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
                function(e:Event):void {
                    // set decoded bitmap data
                    byteSrc.bitmapData = Bitmap(e.target.content).bitmapData;
 
                    // increment images loaded count
                    _loadedImages++;
 
                    // call progress callback if defined
                    if (_onProgress != null) _onProgress(Number(_loadedImages) / _totalImages);
 
                    // if all images loaded
                    if (_totalImages == _loadedImages) {
                        // and onComplete defined
                        if (_onComplete != null)
                            // call it
                            _onComplete();
                    }
                }
            );
 
            // begin load
            loader.loadBytes(ByteArray(new byteSrc));
        }
    }
}

The usage of above is pretty simple, like following

new EmbeddedBitmapDecoder(Assets, imagesDecoded);

where Assets is the class which contains all the embedded images to be decoded, and imagesDecoded is the function which will be called after loading all images.

the other 2 parameters of this class's constructor are

matchRegex - This is the regex which can be used to selectively decode constants from the Asset class. If supplied only constants matching the given regex are decoded.
onProgress - a function taking a single argument, which is the percent of images loaded. This function if supplied is called after each image load, and can be used to make a progress bar.

Once the images are decoded, the resulting BitmapData is stored in the bitmapData property of the Asset.

So for the the following Assets class

package
{
    /**
     * All image assets, embedded as Octet Streams
     */
    public class Assets
    {
        [Embed(source = "../assets/1.png", mimeType = "application/octet-stream")]
        public static const ONE:Class;
 
        [Embed(source = "../assets/2.png", mimeType = "application/octet-stream")]
        public static const TWO:Class;
 
        [Embed(source = "../assets/3.png", mimeType = "application/octet-stream")]
        public static const THREE:Class;
 
        [Embed(source = "../assets/4.png", mimeType = "application/octet-stream")]
        public static const FOUR:Class;
    }
}

We would use the resulting BitmapData like this

Assets.ONE.bitmapData

Thus a simple approach to keep the image sizes in the swf to minimum is to compress ALL PNGs using a program like PNGGauntlet. Then embedding them as octet-streams and decoding at runtime using the OctetStreamBitmapDecoder

The GitHub link for this, with a sample project is this https://github.com/danishgoel/OctetStreamBitmapDecoder

Happy Coding 🙂

Comments (0) Trackbacks (0)

No comments yet.


Leave a comment

No trackbacks yet.