Skip to main content
New Participant
July 9, 2011
Question

Camera image orientation on iOS

  • July 9, 2011
  • 3 replies
  • 23510 views

Hi,

Taking a picture using CamerUI on iOS (and on Android) may bring the image oriented in the wrong angle.

Reading the Exif on Android is fine and allows reorientation to the correct position, however this is not working on iOS.

Since I assume taking a picture using the camera is a core feature for many applications, I’d appreciate if someone who managed to solve it can post a solution here?

Thanks!

    This topic has been closed for replies.

    3 replies

    Known Participant
    October 20, 2015

    The cameraRoll orientation issue is now a bug in bugbase. Upvote if you need a fix. Bug#4070057 - CameraRoll on iOS returns Bitmap in incorrect orientation

    Participating Frequently
    May 15, 2012

    Well, is there any solutions? I still can't fix the camera orientation. I already unchecked Auto orientation in publish settings and tried StageOrientationChange, but no helps.

    That's weird when loadFilePromise doesn't get the correct orrientation of the captured photo.

    Participating Frequently
    May 16, 2012

    GoonNguyen,

    The root of the trouble is that there are two ways to store image orientation information in the JPEG file. In my (admittedly less than comprehensive) experience, most cameras and applications rotate the image data within the file so that the top-left-hand corner of the stored image array is the top-left-hand corner of the picture. In other words, if you took two pictures with the phone camera in opposite orientations, the top left of the picture is stored in the same place in the JPEG file for both.

    On iOS they chose a different way. The image data is stored as it comes out of the sensor and an additional exif data item is added to indicate the orientation. Thus if you took the same two pictures as above, you would find that one of them is inverted in the JPEG file compared to the other. So on iOS, you have to read the EXIF data to tell which way is up. (And reading the EXIF data is problematic since the file isn't really a proper JPEG format, it is a JFIF or some combination of the two. JFIF and JPEG are technically incompatible, though the practical differences are minor.)

    Participating Frequently
    May 17, 2012

    Thank you a lot for you quick reply. After few hours trying and looking around, finnally I can read EXIF of a photo on iOS, actually it's JFIF as you said.

    Allow me to write something here, hope it helps people who still can't get the orientation of the photos on iOS...

    As first, you guys have to take a look at this: http://code.shichiseki.jp/as3/ExifInfo

    And as Joe said above, On iOS, the datasource for the MediaPromise is not file-based, so the file URL is null. Instead, you have to read the image data into a ByteArray in order to access the raw image data. (To just display the image, you could use the Loader.loadMediaPromise() method, but that doesn't give you the EXIF data.)

    Also:

    It also wasn't hard to fix the jp.shichiseki library to ignore the JFIF marker. You just have to add the folowing to the ExifInfo class:

    private const JFIF_MAKER:Array = [0xff, 0xe0]; //new marker type

            //Updated to skip JFIF marker
            private function validate(stream:ByteArray):Boolean {
                var app1DataSize:uint;
                // JPG format check
                if (!hasSoiMaker(stream) ) {
                    return false;
                }
                if(hasJFIFMaker(stream)) //Skip the JFIF marker, if present. CWW
                {
                    stream.position += 16;
                }
                else stream.position -=2; //Set position back to start of APP1 marker
               
                if ( !hasAPP1Maker(stream)) {
                    return false;
                }
                // handle app1 data size
                app1DataSize = stream.readUnsignedShort();
                if (!hasExifHeader(stream)) {
                    return false;
                }
                return true;
            }

            //New function to check for JFIF marker
            private function hasJFIFMaker(stream:ByteArray):Boolean {
                return compareStreamBytes(stream, JFIF_MAKER);
            }

    Note that the library fails silently if it doesn't recognize the file format. That's why you get a null reference error. The library wasn't creating any of its usual objects. Another thing to be aware of is that not all devices on Android record the orientation. In my collection, only one of three did so.

    Finally, below is my few lines of code:

    var mediaPromise:MediaPromise;

    var dataSource:IDataInput;

    var loader:Loader = new Loader();

    loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadComplete);

    addChild(loader);

    captureBtn.addEventListener(MouseEvent.CLICK, function(){

              trace(CameraRoll.supportsBrowseForImage)

              if(CameraRoll.supportsBrowseForImage){

                        camRoll.browseForImage()

              }

    })

    function onSelected(e:MediaEvent){

              mediaPromise = e.data;

              dataSource = mediaPromise.open();

              if( mediaPromise.isAsync )

        {

            trace( "Asynchronous media promise." );

            var eventSource:IEventDispatcher = dataSource as IEventDispatcher;           

            eventSource.addEventListener( Event.COMPLETE, onMediaLoaded );        

        }

        else

        {

            trace( "Synchronous media promise." );

            readMediaData();

        }

              //output.appendText("\n"+mediaPromise)

              //output.appendText("\n"+mediaPromise.file)

              //output.appendText("\n"+mediaPromise.file.url)

              //trace(mediaPromise.file.url);

    }

    function onMediaLoaded( event:Event ):void

    {

        trace("Media load complete");

        readMediaData();

    }

    function readMediaData():void

    {

              // THIS PART IS READING JFIF DATA:

              var data:ByteArray = new ByteArray();

              dataSource.readBytes( data );

              exif = new ExifInfo(data);

              //output.text = displayIFD(exif.ifds.exif); // This stores some properties like resolutionX, resolutionY,.etc.

              //output.text = displayIFD(exif.ifds.primary); // This one stores the orientation data.

              output.text = getOrientation(exif.ifds.primary)

        //do something with the data

              loader.loadFilePromise(mediaPromise);

    }

    function displayIFD(ifd:IFD):String {

              //trace(" --- " + ifd.level + " --- ");

              var str:String = "";

              for (var entry:String in ifd) {

                        trace(entry + ": " + ifd[entry]);

                        str += (entry + ": " + ifd[entry] + "\n")

              }

              return str;

    }

    function getOrientation(ifd:IFD):String{

              var str:String = "";

              for (var entry:String in ifd) {

                        if(entry == "Orientation"){

                                  str = ifd[entry];

                        }

              }

              switch(str){

                        case "1": //normal

                                  str = "NORMAL";

                        break;

                        case "3": //rotated 180 degrees (upside down)

                                  str = "UPSIDE_DOWN";

                        break;

                        case "6": //rotated 90 degrees CW

                                  str = "ROTATED_LEFT"

                        break;

                        case "8": //rotated 90 degrees CCW

                                  str = "ROTATED_RIGHT"

                        break;

                        case "9": //unknown

                                  str = "UNKNOWN"

                        break;

              }

              return str;

    }

    function onCancelled(e:Event){

    }

    Thank you a bunch, again, Joe!

    Have a good day!

    Participating Frequently
    July 20, 2011

    Up !

    Same problem for me.

    How a basic fonction like this is so difficult to find !?

    Did you find anything yet ?

    Cheers.

    Participating Frequently
    July 20, 2011

    This information comes from one of Adobe's quality engineers:

    So I found that the image that we get after using CameraRoll/CameraUI  does contain EXIF information but not in the format expected by the  ExifInfo AS3 library (http://code.shichiseki.jp/as3/ExifInfo/).

    Firstly, it does contain the orientation information which I am able to  verify using the Jpeg decoding tool :  http://www.impulseadventure.com/photo/jpeg-snoop-source.html.

    The Exif data is stored in one of JPEG’s defined utility Application  Segments APPn. The AS3 library - ExifInfo that you are using for reading  Exif header expects APP1 marker whereas the actual image has APP0 at  that place followed by APP1 segment which contains the orientation data.  Images that comply to JFIF standards have APP0 marker just after SOI  (Start of Image) and images that comply to EXIF standards should have  APP1 marker after SOI.

    As per wiki http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format :  "many programs and digital cameras produce files with both application  segments included" which is what exactly happens in our case.

    So I think you may try switching to a library which is able to parse an  image that has multiple APPn markers and read only the one required. You  can even modify the ExifInfo library.

    Hope this helps.

    Participating Frequently
    December 12, 2011

    Joe, I'm interested in taking a look at the ExifReader from http://devstage.blogspot.com/2011/02/extract-jpg-exif-metadata-in.html but that URL now seems invalid.  Any chance you have the source code laying around somewhere??

    Thanks!


    I think this is it:

    // ExifReader.as

    // Reads and obtains JPG EXIF data.

    // Project site: http://devstage.blogspot.com/2011/02/extract-jpg-exif-metadata-in.html

    //

    // SAMPLE USAGE:

    // var reader:ExifReader = new ExifReader();

    // reader.addEventListener(Event.COMPLETE, function (e:Event):void{

    //     trace(reader.getKeys());

    //     trace(reader.getValue("DateTime"));

    //     trace(reader.getThumbnailBitmapData().width +'x' + reader.getThumbnailBitmapData().height);

    //    });

    // reader.load(new URLRequest('myimage.jpg'), true);

    package

    {

        import flash.display.Bitmap;

        import flash.display.BitmapData;

        import flash.display.Loader;

        import flash.display.LoaderInfo;

        import flash.events.Event;

        import flash.events.EventDispatcher;

        import flash.events.ProgressEvent;

        import flash.net.URLRequest;

        import flash.net.URLStream;

        import flash.utils.ByteArray;

        public class ExifReader extends EventDispatcher

        { 

            private var m_loadThumbnail:Boolean = false;

            private var m_urlStream:URLStream = null;

            private var m_data:ByteArray = new ByteArray();

            private var m_exif:Object = new Object;

            private var m_exifKeys:Array = new Array();

            private var m_intel:Boolean=true;

            private var m_loc:uint=0; 

            private var m_thumbnailData:ByteArray = null;

            private var m_thumbnailBitmapData:BitmapData=null;

            private var DATASIZES:Object = new Object;

            private var TAGS:Object = new Object;

            public function load(urlReq:URLRequest, loadThumbnail:Boolean = false):void{

                m_loadThumbnail = loadThumbnail;  

                m_urlStream = new URLStream();

                m_urlStream.addEventListener( ProgressEvent.PROGRESS, dataHandler);

                m_urlStream.load(urlReq);

            }

            public function getKeys():Array{

                return m_exifKeys;

            }

            public function hasKey(key:String):Boolean{

                return m_exif[key] != undefined;

            }

            public function getValue(key:String):Object{

                if(m_exif[key] == undefined) return null;

                return m_exif[key];

            }

            public function getThumbnailBitmapData():BitmapData{

                return m_thumbnailBitmapData;

            }

            public function ExifReader(){

                DATASIZES[1] = 1;

                DATASIZES[2] = 1;

                DATASIZES[3] = 2;

                DATASIZES[4] = 4;

                DATASIZES[5] = 8;

                DATASIZES[6] = 1;  

                DATASIZES[7] = 1;

                DATASIZES[8] = 2;

                DATASIZES[9] = 4;

                DATASIZES[10] = 8;

                DATASIZES[11] = 4;

                DATASIZES[12] = 8;

                TAGS[0x010e] = 'ImageDescription';

                TAGS[0x010f] = 'Make';

                TAGS[0X0110] = 'Model';

                TAGS[0x0112] = 'Orientation';

                TAGS[0x011a] = 'XResolution';

                TAGS[0x011b] = 'YResolution';

                TAGS[0x0128] = 'ResolutionUnit';

                TAGS[0x0131] = 'Software';

                TAGS[0x0132] = 'DateTime';

                TAGS[0x013e] = 'WhitePoint';

                TAGS[0x013f] = 'PrimaryChromaticities';

                TAGS[0x0221] = 'YCbCrCoefficients';

                TAGS[0x0213] = 'YCbCrPositioning';

                TAGS[0x0214] = 'ReferenceBlackWhite';

                TAGS[0x8298] = 'Copyright';

                TAGS[0x829a] = 'ExposureTime';

                TAGS[0x829d] = 'FNumber';

                TAGS[0x8822] = 'ExposureProgram';

                TAGS[0x8827] = 'IsoSpeedRatings';

                TAGS[0x9000] = 'ExifVersion';

                TAGS[0x9003] = 'DateTimeOriginal';

                TAGS[0x9004] = 'DateTimeDigitized';

                TAGS[0x9101] = 'ComponentsConfiguration';

                TAGS[0x9102] = 'CompressedBitsPerPixel';

                TAGS[0x9201] = 'ShutterSpeedValue';

                TAGS[0x9202] = 'ApertureValue';

                TAGS[0x9203] = 'BrightnessValue';

                TAGS[0x9204] = 'ExposureBiasValue';

                TAGS[0x9205] = 'MaxApertureValue';

                TAGS[0x9206] = 'SubjectDistance';

                TAGS[0x9207] = 'MeteringMode';

                TAGS[0x9208] = 'LightSource';

                TAGS[0x9209] = 'Flash';

                TAGS[0x920a] = 'FocalLength';

                TAGS[0x927c] = 'MakerNote';

                TAGS[0x9286] = 'UserComment';

                TAGS[0x9290] = 'SubsecTime';

                TAGS[0x9291] = 'SubsecTimeOriginal';

                TAGS[0x9292] = 'SubsecTimeDigitized';

                TAGS[0xa000] = 'FlashPixVersion';

                TAGS[0xa001] = 'ColorSpace';

                TAGS[0xa002] = 'ExifImageWidth';

                TAGS[0xa003] = 'ExifImageHeight';

                TAGS[0xa004] = 'RelatedSoundFile';

                TAGS[0xa005] = 'ExifInteroperabilityOffset';

                TAGS[0xa20e] = 'FocalPlaneXResolution';

                TAGS[0xa20f] = 'FocalPlaneYResolution';

                TAGS[0xa210] = 'FocalPlaneResolutionUnit';

                TAGS[0xa215] = 'ExposureIndex';

                TAGS[0xa217] = 'SensingMethod';

                TAGS[0xa300] = 'FileSource';

                TAGS[0xa301] = 'SceneType';

                TAGS[0xa302] = 'CFAPattern';

                //... add more if you like.

                //See http://park2.wakwak.com/~tsuruzoh/Computer/Digicams/exif-e.html

            }

            private function dataHandler(e:ProgressEvent):void{

                //EXIF data in top 64kb of data

                if(m_urlStream.bytesAvailable >= 64*1024){

                    m_urlStream.readBytes(m_data, 0, m_urlStream.bytesAvailable);

                    m_urlStream.close();

                    processData();

                }

            }

            private function processData():void{

                var iter:int=0;

                //confirm JPG type

                if(!(m_data.readUnsignedByte()==0xff && m_data.readUnsignedByte()==0xd8))

                    return stop();

                //Locate APP1 MARKER

                var ff:uint=0;

                var marker:uint=0;

                for(iter=0;iter<5;++iter){ //cap iterations

                    ff = m_data.readUnsignedByte();

                    marker = m_data.readUnsignedByte();

                    var size:uint = (m_data.readUnsignedByte()<<8) + m_data.readUnsignedByte();

                    if(marker == 0x00e1) break;

                    else{

                        for(var x:int=0;x<size-2;++x) m_data.readUnsignedByte();

                    }

                }

                //Confirm APP1 MARKER

                if(!(ff == 0x00ff && marker==0x00e1)) return stop();

                //Confirm EXIF header

                var i:uint;

                var exifHeader:Array = [0x45,0x78,0x69,0x66,0x0,0x0];

                for(i=0; i<6;i++) {if(exifHeader != m_data.readByte()) return stop();}

                //Read past TIFF header

                m_intel = (m_data.readByte()!=0x4d);

                m_data.readByte(); //redundant

                for(i=0; i<6;i++) {m_data.readByte();} //read rest of TIFF header

                //Read IFD data

                m_loc = 8;

                readIFD(0);

                //Read thumbnail

                if(m_thumbnailData){

                    var loader:Loader = new Loader();

                    loader.contentLoaderInfo.addEventListener(Event.COMPLETE, thumbnailLoaded);

                    loader.loadBytes(m_thumbnailData);

                }

                else stop();

            }

            //EXIF data is composed of 'IFD' fields.  You have IFD0, which is the main picture data.

            //IFD1 contains thumbnail data.  There are also sub-IFDs inside IFDs, notably inside IFD0.

            //The sub-IFDs will contain a lot of additional EXIF metadata.

            //readIFD(int) will help read all of these such fields.

            private function readIFD(ifd:int):void{

                var iter:int=0;

                var jumps:Array = new Array();

                var subexifJump:uint=0;

                var thumbnailAddress:uint=0;

                var thumbnailSize:int=0;

                // Read number of entries

                var numEntries:uint;

                if(m_intel) numEntries = m_data.readUnsignedByte() + (m_data.readUnsignedByte()<<8);

                else numEntries = (m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte());

                if(numEntries>100) numEntries=100; //cap entries

                m_loc+=2;

                for(iter=0; iter<numEntries;++iter){

                    //Read tag

                    var tag:uint;

                    if(m_intel) tag = (m_data.readUnsignedByte()) + (m_data.readUnsignedByte()<<8);

                    else tag = (m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte());

                    //read type

                    var type:uint;

                    if(m_intel) type = (m_data.readUnsignedByte()+(m_data.readUnsignedByte()<<8));

                    else type = (m_data.readUnsignedByte()<<8)+(m_data.readUnsignedByte()<<0);

                    //Read # of components

                    var comp:uint;

                    if(m_intel) comp = (m_data.readUnsignedByte()+(m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte()<<16) + (m_data.readUnsignedByte()<<24));

                    else comp = (m_data.readUnsignedByte()<<24)+(m_data.readUnsignedByte()<<16) + (m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte()<<0);

                    //Read data

                    var data:uint;

                    if(m_intel) data= m_data.readUnsignedByte()+(m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte()<<16) + (m_data.readUnsignedByte()<<24);

                    else data = (m_data.readUnsignedByte()<<24)+(m_data.readUnsignedByte()<<16) + (m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte()<<0);

                    m_loc+=12;

                    if(tag==0x0201) thumbnailAddress = data; //thumbnail address

                    if(tag==0x0202) thumbnailSize = data;  //thumbnail size (in bytes)

                    if(TAGS[tag] != undefined){

                        //Determine data size

                        if(DATASIZES[type] * comp <= 4){

                            //data is contained within field

                            m_exif[TAGS[tag]] = data;

                            m_exifKeys.push(TAGS[tag]);

                        }

                        else{

                            //data is at an offset

                            var jumpT:Object = new Object();

                            jumpT.name = TAGS[tag];

                            jumpT.address=data;

                            jumpT.components = comp;

                            jumpT.type = type;

                            jumps.push(jumpT);

                        }

                    }   

                    if(tag==0x8769){ // subexif tag

                        subexifJump = data;

                    }

                }

                var nextIFD:uint;

                if(m_intel) {

                    nextIFD= m_data.readUnsignedByte()+(m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte()<<16) + (m_data.readUnsignedByte()<<24);

                }

                else {

                    nextIFD = (m_data.readUnsignedByte()<<24)+(m_data.readUnsignedByte()<<16) + (m_data.readUnsignedByte()<<8) + (m_data.readUnsignedByte()<<0);

                }

                m_loc+=4;

                //commenting this out, as suggested in the comments.

                //if(ifd==0) jumps = new Array();

                for each(var jumpTarget:Object in jumps){

                    var jumpData:Object = null;

                    for(;m_loc<jumpTarget.address;++m_loc)m_data.readByte();

                    if(jumpTarget.type==5){

                        //unsigned rational

                        var numerator:uint = m_data.readInt();

                        var denominator:uint = m_data.readUnsignedInt();

                        m_loc+=8;

                        jumpData = numerator / denominator;

                    }

                    if(jumpTarget.type==2){

                        //string

                        var field:String='';

                        for(var compGather:int=0;compGather<jumpTarget.components;++compGather){

                            field += String.fromCharCode(m_data.readByte());

                            ++m_loc;

                        }

                        if(jumpTarget.name=='DateTime' ||

                            jumpTarget.name=='DateTimeOriginal' ||

                            jumpTarget.name=='DateTimeDigitized'){

                            var array:Array = field.split(/[: ]/);

                            if(parseInt(array[0]) > 1990){

                                jumpData = new Date(parseInt(array[0]), parseInt(array[1])-1,

                                    parseInt(array[2]), parseInt(array[3]),

                                    parseInt(array[4]), parseInt(array[5]));

                            }

                        }

                        else jumpData = field;

                    }

                    m_exif[jumpTarget.name] = jumpData;

                    m_exifKeys.push(jumpTarget.name);

                }

                if(ifd==0 && subexifJump){

                    //jump to subexif area to obtain meta information

                    for(;m_loc<data;++m_loc) m_data.readByte();

                    readIFD(ifd);

                }

                if(ifd==1 && thumbnailAddress && m_loadThumbnail){

                    //jump to obtain thumbnail

                    for(;m_loc<thumbnailAddress;++m_loc) m_data.readByte();

                    m_thumbnailData = new ByteArray();

                    m_data.readBytes(m_thumbnailData, 0, thumbnailSize);

                    return;

                }

                // End-of-IFD

                // read the next IFD

                if(nextIFD){

                    for(;m_loc<nextIFD;++m_loc) m_data.readUnsignedByte();               

                }

                if(ifd==0 && nextIFD)

                {

                    readIFD(1);

                }

            }

            private function thumbnailLoaded(e:Event):void{

                m_thumbnailData.clear();

                var loader:LoaderInfo = e.target as LoaderInfo;

                var bitmap:Bitmap = loader.content as Bitmap;

                if(bitmap != null){

                    m_thumbnailBitmapData = bitmap.bitmapData;

                }

                stop();

            }

            // Releases m_data and dispatches COMPLETE signal.

            private function stop():void{

                m_data=null;

                dispatchEvent(new Event(Event.COMPLETE));

                return;

            }

        }

    }