Yukihana (雪花) embroidery on a naginata jacket
雪 (yuki) = snow, 花 (hana) = flower, in Japanese, can also be read as "Yukka".

XIFF chat - Part 6: Avatar image

Example number six in the voyage of the AS3 chat application made with XIFF library shows the way how the avatar image of a user can be retrieved.

The specification related to the topic would be XEP-0084. Unfortunately the current implementation in XIFF is done the way XEP-0153 defines it, thus following XEP-0054.

Although there is nothing wrong with this, it would be nice to do everything in the sense of the best practise. Sadly the best practise is not always the one which becomes most successful, meaning the amount of clients and servers supporting it.

/**
 * @mxmlc -target-player=10.0.0 -debug
 */
package
{
    import flash.display.*;
    import flash.events.*;
    import flash.geom.Matrix;
    import flash.text.*;
    import flash.utils.*;
    import flash.system.Security;
    import flash.ui.Keyboard;

    import org.igniterealtime.xiff.conference.*;
    import org.igniterealtime.xiff.core.*;
    import org.igniterealtime.xiff.data.*;
    import org.igniterealtime.xiff.data.disco.*;
    import org.igniterealtime.xiff.data.im.*;
    import org.igniterealtime.xiff.events.*;
    import org.igniterealtime.xiff.exception.*;
    import org.igniterealtime.xiff.im.*;
    import org.igniterealtime.xiff.util.*;
    import org.igniterealtime.xiff.vcard.VCard;
    import org.igniterealtime.xiff.data.vcard.*;

    [SWF(backgroundColor = '0x5B5B5B', frameRate = '33', width = '600', height = '400')]

    /**
     * Example of the vcard based avatar image acquisition.
     */
    public class XIFFstep6 extends Sprite
    {
        private const SERVER:String = "192.168.1.37";
        private const PORT:int = 5222;
        private const USERNAME:String = "flasher";
        private const PASSWORD:String = "flasher";
        private const RESOURCE_NAME:String = "AvatarTest";
        private const CHECK_POLICY:Boolean = true;
        private const POLICY_PORT:uint = 5229;
        private const COMPRESS:Boolean = false;

        private var _connection:XMPPConnection;
        private var _roster:Roster;
        private var _vcards:Array = [];
        private var _keepAlive:Timer;
        private var _rosterCont:Sprite;
        private var _statField:TextField;
        private var _statTimer:Timer;
        private var _format:TextFormat = new TextFormat("Verdana", 12, 0xF4F4F4);

        public function XIFFstep6()
        {
            stage.align = StageAlign.TOP_LEFT;
            stage.scaleMode = StageScaleMode.NO_SCALE;

            loaderInfo.addEventListener(Event.INIT, onInit);
        }

        private function onInit(event:Event):void
        {
            if (CHECK_POLICY)
            {
                Security.loadPolicyFile("xmlsocket://" + SERVER + ":" + POLICY_PORT);
            }

            _statField = new TextField();
            _statField.name = "statField";
            _statField.defaultTextFormat = _format;
            _statField.multiline = true;
            _statField.wordWrap = true;
            _statField.width = 300;
            _statField.height = 40;
            addChild(_statField);

            _statTimer = new Timer(2 * 1000);
            _statTimer.addEventListener(TimerEvent.TIMER, onStatTimer);
            _statTimer.start();

            connect();
        }

        private function connect():void
        {
            _connection = new XMPPConnection();
            _connection.addEventListener(XIFFErrorEvent.XIFF_ERROR, onXiffError);
            _connection.addEventListener(LoginEvent.LOGIN, onLogin);
            _connection.addEventListener(PresenceEvent.PRESENCE, onPresence);
            _connection.addEventListener(DisconnectionEvent.DISCONNECT, onDisconnect);

            _connection.useAnonymousLogin = false;
            _connection.username = USERNAME;
            _connection.password = PASSWORD;
            _connection.server = SERVER;
            _connection.port = PORT;
            _connection.resource = RESOURCE_NAME;
            _connection.compress = COMPRESS;
            _connection.connect();

            _roster = new Roster(_connection);
            _roster.addEventListener(RosterEvent.ROSTER_LOADED, onRoster);
            _roster.addEventListener(RosterEvent.USER_ADDED, onRoster);

            _keepAlive = new Timer(2 * 60 * 1000);
            _keepAlive.addEventListener(TimerEvent.TIMER, onKeepAliveLoop);
        }

        private function visualiseRoster():void
        {
            _rosterCont = new Sprite();
            _rosterCont.name = "rosterCont";
            _rosterCont.y = _statField.height;
            addChild(_rosterCont);

            var items:Array = _roster.toArray();
            var len:uint = items.length;
            for (var i:uint = 0; i < len; ++i)
            {
                var item:RosterItemVO = items[i] as RosterItemVO;
                var box:Sprite = createUserBox(item);
                box.x = 10 + 40 * i;
                box.y = 10 + 30 * i;
                _rosterCont.addChild(box);

                var vCard:VCard = VCard.getVCard(_connection, item);
                vCard.addEventListener(VCardEvent.LOADED, onVCard);
                vCard.addEventListener(VCardEvent.AVATAR_LOADED, onVCard);
                vCard.addEventListener(VCardEvent.ERROR, onVCard);
                _vcards.push(vCard);
            }
        }

        private function createUserBox(item:RosterItemVO):Sprite
        {
            var sp:Sprite = new Sprite();
            sp.name = item.jid.bareJID;
            sp.mouseChildren = false;
            sp.buttonMode = true;

            var bm:Bitmap = new Bitmap(new BitmapData(100, 100));
            bm.name = "avatar";
            bm.bitmapData.perlinNoise(40, 60, 4, 4, true, false);
            bm.x = 2;
            bm.y = 2;
            sp.addChild(bm);

            var tx:TextField = new TextField();
            tx.name = "jid";
            tx.defaultTextFormat = _format;
            tx.text = item.jid.bareJID;
            tx.multiline = true;
            tx.wordWrap = true;
            tx.x = 104;
            tx.y = 2;
            tx.width = 170;
            sp.addChild(tx);

            drawBoxBackground(sp);

            sp.addEventListener(MouseEvent.MOUSE_OVER, onMouse);
            sp.addEventListener(MouseEvent.MOUSE_OUT, onMouse);
            sp.addEventListener(MouseEvent.MOUSE_DOWN, onMouse);
            sp.addEventListener(MouseEvent.MOUSE_UP, onMouse);
            return sp;
        }

        private function drawBoxBackground(sp:Sprite, color:uint = 0x3A3A3A):void
        {
            var gr:Graphics = sp.graphics;
            gr.clear();
            gr.beginFill(color);
            gr.lineStyle(1, 0x1A1A1A);
            gr.drawRoundRect(0, 0, sp.width + 4, sp.height + 4, 8, 8);
            gr.endFill();
        }

        private function updateUserBox(item:VCard):void
        {
            var loader:Loader = new Loader();
            loader.name = item.jid.bareJID;
            loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onAvatarLoaderComplete);
            loader.loadBytes(item.avatar);
        }

        private function onAvatarLoaderComplete(event:Event):void
        {
            var info:LoaderInfo = event.target as LoaderInfo;
            var loader:Loader = info.loader;

            var sp:Sprite = _rosterCont.getChildByName(loader.name) as Sprite;
            if (sp != null)
            {
                var bm:Bitmap = sp.getChildByName("avatar") as Bitmap;

                // Wanna get a square
                var scale:Number = Math.max(bm.width / loader.width, bm.height / loader.height);
                var mat:Matrix = new Matrix();
                mat.scale(scale, scale);
                mat.tx = bm.width - scale * loader.width;
                mat.ty = bm.height - scale * loader.height;
                trace("onAvatarLoaderComplete. mat: " + mat);

                bm.bitmapData.draw(loader, mat);
            }
        }

        private function onVCard(event:VCardEvent):void
        {
            trace("onVCard. " + event.toString());
            if (event.type == VCardEvent.AVATAR_LOADED)
            {
                updateUserBox(event.vcard);
            }
            else if (event.type == VCardEvent.ERROR)
            {
                // Triggered only by _vCardSent in VCard.
                trace("onVCard. ERROR. " + event.vcard);
            }
        }

        private function onPresence(event:PresenceEvent):void
        {
            trace("onPresence. " + event.toString());
            var len:uint = event.data.length;
            for (var i:uint = 0; i < len; ++i)
            {
                var presence:Presence = event.data[i] as Presence;
                trace("onPresence. " + i + " show: " + presence.show);
                trace("onPresence. " + i + " type: " + presence.type);
                trace("onPresence. " + i + " status: " + presence.status);
                trace("onPresence. " + i + " from: " + presence.from);
                trace("onPresence. " + i + " to: " + presence.to);

                switch (presence.type)
                {
                    case Presence.SUBSCRIBE_TYPE :
                        // Automatically add all those to _roster whom have requested to be our friend.
                        _roster.grantSubscription(presence.from.unescaped, true);
                        break;
                    case Presence.SUBSCRIBED_TYPE :
                        break;
                }
            }
        }

        private function onRoster(event:RosterEvent):void
        {
            trace("onRoster. " + event.toString());
            switch (event.type)
            {
                case RosterEvent.ROSTER_LOADED :
                    visualiseRoster();
                    break;
                case RosterEvent.USER_ADDED :
                    break;
            }
        }

        private function onMouse(event:MouseEvent):void
        {
            var sp:Sprite = event.target as Sprite;
            var color:uint = 0x3A3A3A;
            if (event.type == MouseEvent.MOUSE_OVER)
            {
                color = 0x9A9A9A;
                _rosterCont.swapChildren(sp, _rosterCont.getChildAt(_rosterCont.numChildren - 1));
            }
            else if (event.type == MouseEvent.MOUSE_DOWN)
            {
                color = 0x121212;
                sp.startDrag();
            }
            else if (event.type == MouseEvent.MOUSE_UP)
            {
                color = 0x9A9A9A;
                sp.stopDrag();
            }
            drawBoxBackground(sp, color);
        }

        private function onKeepAliveLoop(event:TimerEvent):void
        {
            _connection.sendKeepAlive();
        }

        private function onStatTimer(event:TimerEvent):void
        {
            if (_connection != null)
            {
                _statField.text = "Incoming: " + Math.round(_connection.incomingBytes / 1024) + " KB\n"
                    + "Outgoing: " + Math.round(_connection.outgoingBytes / 1024) + " KB";
            }
        }

        private function onLogin(event:LoginEvent):void
        {
            trace("onLogin. " + event.toString());
            _keepAlive.start();

            var presence:Presence = new Presence(null, _connection.jid.escaped, null, null, null, 1);
            _connection.send(presence);
        }

        private function onDisconnect(event:DisconnectionEvent):void
        {
            trace("onDisconnect. " + event.toString());
            _keepAlive.stop();
        }

        private function onXiffError(event:XIFFErrorEvent):void
        {
            trace("onXiffError. " + event.toString());
            trace("onXiffError.errorMessage: " + event.errorMessage);
        }
    }
}

Not all clients/servers limit the size of the avatar image, thus it can be really huge in some cases. That is why there is the statistics of the incoming and the outgoing data amounts.

Suggested reading related to the avatar data transfer:

Please note that the stream compression is not yet fully implemented to date, but will be very handy specially for cases like avatar image transfer.