Lab

Encoder du BitmapData avec un Worker AS3

Bien que la classe AS3 Worker soit disponible depuis un moment, son introduction sur iOS est récente : moins de 6 mois. Les workers AS3 étant désormais disponibles partout, il est temps de créer de petites bibliothèques…

Bien que la classe AS3 Worker soit disponible depuis un moment, son introduction sur iOS est récente : moins de 6 mois. Les workers AS3 étant désormais disponibles partout, il est temps de créer de petites bibliothèques multi-threadées en quelques lignes de code seulement !

Si vous n’avez jamais joué avec les Worker, vous devriez jeter un œil à cette excellente série d’articles de blog.

Il existe plusieurs façons de créer un Worker, mais si vous ne voulez pas tomber dans un piège en utilisant des ANEs dans votre projet, je recommande de les utiliser via un SWF chargé.

Voici donc un petit exemple créant une bibliothèque simple pour encoder du bitmap data et sauvegarder des images sur le disque via un Worker :

L’idée est que notre application principale récupère plusieurs Bitmap depuis des URLs et que l’on souhaite les sauvegarder dans applicationStorageDirectory sans lag d’UI. Les Workers permettent une communication efficace entre eux via ByteArray. Ainsi, une fois notre Bitmap obtenu, nous le transformons en ByteArray partageable (shareable, entre Workers) que nous envoyons à notre ImageWriterWorker. Ce ByteArray est ensuite transformé en BitmapData pour être encodé en PNG ou JPG selon l’extension du fichier. Et enfin, nous recevons un autre ByteArray que nous sauvegardons sur le disque !

Commençons par notre classe principale, qui charge un futur SWF et crée le worker.

package {

	import flash.display.Bitmap;
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.net.URLRequest;
	import flash.system.ApplicationDomain;
	import flash.system.LoaderContext;
	import flash.system.MessageChannel;
	import flash.system.Worker;
	import flash.system.WorkerDomain;
	import flash.utils.ByteArray;

	[SWF(frameRate = "60", width = "1024", height = "468", backgroundColor = "0xFFFFFF")]

	public class Main extends Sprite {

		public var worker:Worker;
		public var msgChannelMainToImageWriterWorker:MessageChannel;
		public var workerByteArrayShared:ByteArray;

		public function Main() {
			super();

			var workerLoader:Loader = new Loader();

			//we specify a loader context for managing SWF loading on iOS.
			var loaderContext:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain);

			workerLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, _onImageWriterWorkerLoaded);
			workerLoader.load(new URLRequest("workers/ImageWriterWorker.swf"), loaderContext);
		}

		private function _onImageWriterWorkerLoaded(evt:Event):void {

			evt.target.removeEventListener(Event.COMPLETE, _onImageWriterWorkerLoaded);

			var workerBytes:ByteArray = evt.target.bytes;
			worker = WorkerDomain.current.createWorker(workerBytes, true);
			// we set the latest arguments to true, because we want to be able to save on disk via the Worker.

			msgChannelMainToImageWriterWorker = Worker.current.createMessageChannel(worker);

			worker.setSharedProperty("mainToImageWriterWorker", msgChannelMainToImageWriterWorker);

			workerByteArrayShared = new ByteArray();
			workerByteArrayShared.shareable = true; // we share the byte array through the worker
			worker.setSharedProperty("imageBytes", workerByteArrayShared);

			worker.start();
		}
	}
}

Voici maintenant le worker de notre bibliothèque :

package {

	import flash.display.BitmapData;
	import flash.display.JPEGEncoderOptions;
	import flash.display.PNGEncoderOptions;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.filesystem.File;
	import flash.filesystem.FileMode;
	import flash.filesystem.FileStream;
	import flash.system.MessageChannel;
	import flash.system.Worker;
	import flash.utils.ByteArray;

	public class ImageWriterWorker extends Sprite {

		public var msgChannelMainToImageWriterWorker:MessageChannel;

		public var bytes:ByteArray;

		public function ImageWriterWorker() {

			msgChannelMainToImageWriterWorker = Worker.current.getSharedProperty("mainToImageWriterWorker");

			// simple check to remove errors on execution without the main worker
			if (msgChannelMainToImageWriterWorker) {

				msgChannelMainToImageWriterWorker.addEventListener(Event.CHANNEL_MESSAGE, messageFromMainWorker);

				bytes = worker.getSharedProperty("imageBytes");
			}
		}

		private function messageFromMainWorker(evt:Event):void {

			if (msgChannelMainToImageWriterWorker.messageAvailable) {

				// we receive an object composed with an image width, height, and a url path.
				var obj:Object = msgChannelMainToImageWriterWorker.receive();

				bytes.position = 0; // read informations from start.

				var bmpd:BitmapData = new BitmapData(obj.width, obj.height, true, 0xFFFFFF);
				bmpd.setPixels(bmpd.rect, bytes);

				var localFile:File = new File(obj.path);

				var extension = obj.path.substring(obj.path.lastIndexOf(".") + 1, obj.path.length);
				var bytesEncoded:ByteArray;

				if (extension == "jpg")
					bytesEncoded = bmpd.encode(bmpd.rect, new JPEGEncoderOptions(70));

				else if (extension == "png")
					bytesEncoded = bmpd.encode(bmpd.rect, new PNGEncoderOptions(true));

				else
					throw "Unknow extension: " + extension;

				var stream:FileStream = new FileStream();
				stream.open(localFile, FileMode.WRITE);
				stream.writeBytes(bytesEncoded);
				stream.close();

				bytesEncoded.clear();
			}
		}
	}
}

Voyons maintenant comment communiquer depuis notre worker principal vers notre bibliothèque ImageWriterWorker :

private function loader_completeHandler(evt:Event):void {

	var bmp:Bitmap = evt.target.content as Bitmap;

	Main.workerByteArrayShared.clear(); // we don't want the system to get out of memory!

	bmp.bitmapData.copyPixelsToByteArray(bmp.bitmapData.rect, Main.workerByteArrayShared);

	Main.workerMessageChannelMainToBack.send({width:bmp.width, bmp:bmpd.height, path:File.applicationStorageDirectory.resolvePath("images/1.jpg").nativePath});
}

Le ByteArray n’a pas besoin de passer par le MessageChannel puisqu’il est partageable ! Et finalement, c’est notre worker principal qui fournit le chemin complet, car si nous essayons d’accéder à applicationStorageDirectory depuis notre thread principal, nous obtenons un chemin qui ne fonctionne pas : /Users/Aymeric/Library/Application Support/[Worker].null/Local Store/images/1.jpg.

Est-ce que tout est terminé ? Oui, ça pourrait bien fonctionner si nous téléchargeons des images etc., mais en fait il nous manque quelque chose d’important : la concurrence ! Si notre ImageWriterWorker met plus de temps à encoder que ne dure le copyPixelsToByteArray, nous modifions le ByteArray avant (voire pendant) l’encodage via setPixels ! Nous pourrions, en effet, envoyer aussi le ByteArray dans le message, mais c’est lourd. Nous conservons donc la propriété shareable et implémentons une file d’attente. L’ImageWriterWorker enverra un message une fois qu’il aura encodé et sauvegardé une image, afin que nous puissions traiter l’élément suivant de la file. Implémentons donc un message channel dans l’autre sens :

// Main.as
msgChannelImageWriterToMainWorker = worker.createMessageChannel(Worker.current);

msgChannelImageWriterToMainWorker.addEventListener(Event.CHANNEL_MESSAGE, messagesFromImageWriterWorker);

worker.setSharedProperty("imageWriterWorkerToMain", msgChannelImageWriterToMainWorker);

public function messagesFromImageWriterWorker(evt:Event):void {

	if (msgChannelImageWriterToMainWorker.messageAvailable) {

		if (msgChannelImageWriterToMainWorker.receive() == "IMAGE_SAVED") {

			processBitmapQueueToImageWriterWorker();
		}
	}
}

// ImageWriterWorker.as

msgChannelImageWriterToMainWorker = Worker.current.getSharedProperty("imageWriterWorkerToMain");

// then once the stream for saving the image is done:
msgChannelImageWriterToMainWorker.send("IMAGE_SAVED");

// let's define our queue in an array:
bitmapsToEncode.push([new Image0(), File.applicationStorageDirectory.resolvePath("images/0.jpg").nativePath]);

public function processBitmapQueueToImageWriterWorker():void {

	if (bitmapsToEncode.length < 1) {
		trace("queue finished");
		return;
	}

	var tab:Array = bitmapsToEncode.shift();

	var bitmap:Bitmap = tab[0];

	workerByteArrayShared.clear();

	bitmap.bitmapData.copyPixelsToByteArray(bitmap.bitmapData.rect, workerByteArrayShared);

	msgChannelMainToImageWriterWorker.send({width:bitmap.width, height:bitmap.height, path:tab[1]});
}

Voilà, c’est terminé ! Vous pouvez télécharger le code.

Une classe Worker multiplateforme était l’un des derniers rouages nécessaires à la plateforme AS3 ! Oui, mais comme l’UI se trouve sur le thread principal, on ne peut pas uploader notre texture sur le GPU sans freeze… ? Exact. Mais Adobe travaille sur le rouage ultime, un upload asynchrone pour les textures ! Gardez la foi ;)

AL
Aymeric Lamboley
Fondateur & Directeur technique