iPhone で撮影した写真の Exif を AIR for iOS で取得するためのメモ


AIR for iOS で作成しているアプリで、iPhone で撮影した写真の向きを調べるため Exif を取得しようとしたら、予想以上に手こずってひどい目に遭ったので忘れないようにメモ。

JPEG 画像から Exif を取得できるライブラリとして Exif library AS3 がありますが、このライブラリを使用して iPhone で撮影した写真の Exif を取得して Orientation を調べようとしましたが何故かうまくいきませんでした。
(参考:Exif の Orientation 一覧)

こりゃこまったなーと思いながら色々調べてみると…

Reading exif data on iOS (Adobe AIR) というタイトルのブログの記事を見つけて、ここ書いてあるとおりに ExifInfo のソースを書き直すと無事読み込めるようになりました。

どのように動作するか見てみたい方は、AIR for iOS で試しに作ったアプリ「トイカム」で、この Exif から写真の向きを読み取る仕組みを使っていますので、実際の動作はこちらで確認できます。

この記事によると、iOS の写真から Exif を取得する場合は ”To fix the problem, you have to check to see if the first marker after the initial JPEG marker is the JFIF one. If it is, skip it. That’s all there is too it. ” とあります。

どうやら iOS の場合、JPEGファイルの最初のマーカーが他のJPEGファイルと違っているのでそれをスキップ(バイナリのポジションを移動)する必要があるようです。
(詳しいことはよく分かっていませんが…)

あと、Exif を読み込む際の MediaPromise の使い方もよく分からなかったのですが、先ほどの記事のコメントが参考になりました。

MediaPromise から Exif を取得する場合は、MediaPromise を open して IDataInput で参照したあと ByteArray に読み込んで、その ByteArray から Exif を取得、実画像も Loader.loadFilePromise は使用せず、ByteArray を Loader.loadBytes で読み込みます。
この時、ByteArray のポジションは先に Exif の取得で使用しているので念のため 0 に戻しました。

というわけで、今回の検証用コードです。
( ExifInfo は先に紹介した記事の通りに書き直して使用しました)

ドキュメントクラス

package
{
	import flash.display.Bitmap;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	
	import jp.digifie.media.CameraManeger;
	
	[SWF( width = '640', height = '960', frameRate = '30', backgroundColor = '0' )]
	
	public class ExifTest extends Sprite
	{
		
		private var _cameraManager:CameraManeger;
		private var _cameraImage:Bitmap = new Bitmap();
		
		public var cameraBtn:CameraBtn;
		public var cameraRollBtn:CameraRollBtn;
		
		public function ExifTest()
		{
			super();
			
			_cameraManager = CameraManeger.getInstance();
			addChild( _cameraImage );
			
			cameraBtn = new CameraBtn();
			cameraBtn.x = 20;
			cameraBtn.y = 960 - cameraBtn.height - 20;
			
			cameraRollBtn = new CameraRollBtn();
			cameraRollBtn.x = cameraBtn.x + cameraRollBtn.width + 20;
			cameraRollBtn.y = cameraBtn.y;
			
			addChild( cameraBtn );
			addChild( cameraRollBtn );
			
			cameraBtn.addEventListener( MouseEvent.CLICK, addCamera );
			cameraRollBtn.addEventListener( MouseEvent.CLICK, addCameraRoll );
		}
		
		
		private function addCamera( e:MouseEvent ):void
		{
			_cameraManager.addCameraUI();
			_cameraManager.addEventListener( "imageselect_done", showCameraImage );
		}
		
		
		private function addCameraRoll( e:MouseEvent ):void
		{
			_cameraManager.addCameraRoll();
			_cameraManager.addEventListener( "imageselect_done", showCameraImage );
		}
		
		
		private function showCameraImage( e:Event ):void
		{
			_cameraManager.removeEventListener( "imageselect_done", showCameraImage );
			_cameraImage.bitmapData = _cameraManager.resizedImage( 640, 960 );
		}
		
		
	}
}

カメラおよびカメラロールの制御クラス

package jp.digifie.media
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Loader;
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.MediaEvent;
	import flash.geom.Matrix;
	import flash.media.CameraRoll;
	import flash.media.CameraUI;
	import flash.media.MediaPromise;
	import flash.media.MediaType;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.IDataInput;
	
	import jp.shichiseki.exif.*;

	public class CameraManeger extends EventDispatcher
	{

		private static var instance:CameraManeger;

		public var cameraUI:CameraUI = new CameraUI();
		public var cameraRoll:CameraRoll = new CameraRoll();

		private var _imageLoader:Loader;
		private var _image:BitmapData;
		
		private var imagePromise:MediaPromise;
		private var imageOrientation:int;


		public function CameraManeger( enforcer:SingletonEnforcer )
		{
			setup();
		}


		// ----------- Singleton GetInstance -------------------------------------- /
		public static function getInstance():CameraManeger
		{
			if ( !CameraManeger.instance ) CameraManeger. instance = new CameraManeger( new SingletonEnforcer());
			return CameraManeger.instance;
		}


		// ----------- Setup -------------------------------------- /
		private function setup():void
		{
			if ( CameraUI.isSupported )
			{
				cameraUI.addEventListener( MediaEvent.COMPLETE, onImageCapture );
				cameraUI.addEventListener( Event.CANCEL, onCaptureCanceled );
				cameraUI.addEventListener( ErrorEvent.ERROR, cameraError );
			}

			if ( CameraRoll.supportsBrowseForImage )
			{
				cameraRoll.addEventListener( MediaEvent.SELECT, onImageCapture );
				cameraRoll.addEventListener( Event.CANCEL, onCaptureCanceled );
				cameraRoll.addEventListener( ErrorEvent.ERROR, cameraError );
			}
		}



		// ----------- Add CameraUI or CameraRoll -------------------------------------- /
		public function addCameraUI():void
		{
			if ( CameraUI.isSupported )
				cameraUI.launch( MediaType.IMAGE );
		}

		public function addCameraRoll():void
		{
			if ( CameraRoll.supportsBrowseForImage )
				cameraRoll.browseForImage();
		}

		
		private var _dataSrc:IDataInput;
		private var _exif:ExifInfo;

		// ----------- MediaEvent -------------------------------------- /

		private function onImageCapture( e:MediaEvent ):void
		{
			imagePromise = e.data;
			_dataSrc = imagePromise.open();

			if ( imagePromise.isAsync )
			{
				var eventSource:IEventDispatcher = _dataSrc as IEventDispatcher;
				eventSource.addEventListener( Event.COMPLETE, readExif );
			}
			else
			{
				readExif();
			}
		}

		// ----------- Exif -------------------------------------- /
		private function readExif( e:Event = null ):void
		{
			var ba:ByteArray = new ByteArray();
			_dataSrc.readBytes( ba );
			
			_exif = new ExifInfo( ba );
			
			if ( _exif.ifds.primary )
				displayIFD( _exif.ifds.primary );
			if ( _exif.ifds.exif )
				displayIFD( _exif.ifds.exif );
			if ( _exif.ifds.gps )
				displayIFD( _exif.ifds.gps );
			if ( _exif.ifds.interoperability )
				displayIFD( _exif.ifds.interoperability );
			if ( _exif.ifds.thumbnail )
				displayIFD( _exif.ifds.thumbnail );
			
			imageOrientation = _exif.ifds.primary.Orientation;
			
			ba.position = 0;
			
			_imageLoader = new Loader();
			_imageLoader.contentLoaderInfo.addEventListener( Event.COMPLETE, onLoadImageComplete );
			_imageLoader.addEventListener( IOErrorEvent.IO_ERROR, cameraError );
			_imageLoader.loadBytes( ba );
		}

		// Tracer
		private function displayIFD( ifd:IFD ):void
		{
			trace( " --- " + ifd.level + " --- " );
			for ( var entry:String in ifd )
			{
				trace( entry + ": " + ifd[ entry ]);
			}
		}
		
		// ImageLoaded
		private function onLoadImageComplete( e:Event ):void
		{
			imageForBitmapData( _imageLoader.content as Bitmap );
			dispatchEvent( new Event( "imageselect_done" ));
		}
		
		// Cancel
		private function onCaptureCanceled( e:Event ):void
		{
			dispatchEvent( new Event( "canceled_imageselect" ));
		}
		
		// Error
		private function cameraError( e:ErrorEvent ):void
		{
			dispatchEvent( new Event( "camera_error" ));
		}




		// ----------- Image for BitmapData -------------------------------------- /
		private function imageForBitmapData( bm:Bitmap ):void
		{
			_image = bm.bitmapData.clone();
			_imageLoader = null;
		}



		// ----------- Image Export -------------------------------------- /
		public function resizedImage( w:Number, h:Number ):BitmapData
		{
			var image:BitmapData = new BitmapData( 640, 960, false, 0 );
			var ratio:Number = 1;
			var matrix:Matrix;
			var ofsetY:Number;

			if( imageOrientation == 1)
			{
				ratio = 640 / _image.width;
				ofsetY = ( 960 - _image.height * ratio ) * .5;
				matrix = new Matrix( ratio, 0, 0, ratio );
				matrix.translate( 0, ofsetY );
			}
			else if( imageOrientation == 3  )
			{
				ratio = 640 / _image.width;
				ofsetY = ( 960 - _image.height * ratio ) * .5;
				matrix = new Matrix( ratio, 0, 0, ratio );
				matrix.rotate( Math.PI );
				matrix.translate( 640, _image.height * ratio + ofsetY );
			}
			else if( imageOrientation == 6  )
			{
				ratio = 960 / _image.width;
				
				matrix = new Matrix( ratio, 0, 0, ratio );
				matrix.rotate( Math.PI / 180 * 90 );
				matrix.translate( 640, 0 );
			}
			else
			{
				ratio = 640 / _image.width;
				ofsetY = ( 960 - _image.height * ratio ) * .5;
				matrix = new Matrix( ratio, 0, 0, ratio );
				matrix.translate( 0, ofsetY );
			}


			image.draw( _image, matrix, null, null, null, true );
			_image.dispose();

			return image;
		}
	}
}

//
class SingletonEnforcer
{
}

あ、ちなみにこのコードで Android 端末でも試してみたのですが、iPhone と同様に Exif が取得できました。