Skip navigation
Currently Being Moderated

Camera image orientation on iOS

Jul 9, 2011 2:17 AM

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!

 
Replies
  • Currently Being Moderated
    Jul 20, 2011 3:07 AM   in reply to nirnirnir

    Up !

     

    Same problem for me.

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

    Did you find anything yet ?

     

    Cheers.

     
    |
    Mark as:
  • Currently Being Moderated
    Jul 20, 2011 10:18 AM   in reply to WeLoveKimchi

    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.

     
    |
    Mark as:
  • Currently Being Moderated
    Jul 20, 2011 10:27 PM   in reply to Joe ... Ward

    Thanks a lot for your answer.

    I understand the concept of switching the APP marker to get data, but i already have trouble about accessing picture.

     

    For exemple if i take the sample from ExifInfo AS3 Library (just the part about accessing the image), here is my test

     

    private function mediaSelectHandler( event:MediaEvent ):void
    {
            mediaPromise = event.data;
            trace(mediaPromise);
            trace(mediaPromise.file.url);

            /*
            this.exifLoader = new ExifLoader();
            this.exifLoader.addEventListener(Event.COMPLETE, completeHandler );
            this.exifLoader.load( new URLRequest( mediaPromise.file.url ) );

            */

    }

     

    When media is selected, when i trace mediaPromise, i get a media [mediaPromise object], but when i trace mediaPromise.file.url its null. In that way how can i get access to the file because since on iOS its null.

     

    Is there any example/sample about this, because i really get the concept and the logic, but i have no idea how to get it.

     

    Thank you again

     
    |
    Mark as:
  • Currently Being Moderated
    Jul 21, 2011 2:33 PM   in reply to WeLoveKimchi

    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.)

     

    For an example of how to get the raw image data, please see my code example in this thread: http://forums.adobe.com/message/3667058#3667058.

     
    |
    Mark as:
  • Currently Being Moderated
    Jul 21, 2011 9:51 PM   in reply to Joe ... Ward

    Thanks a lot for all of these informations.

    You are provided me a really big solution. But unfortunately i didnt arrive yet to get the orientation.

     

    As a test i used the sample you gave me, that get the picture as a ByteArray.

    I edited and checked how the Exif library was working, and indeed after getting the URL of the image, it is loading its ByteArray

     

    This is from exifLoader.as (from the library http://code.shichiseki.jp/as3/ExifInfo/)

     

            private function onComplete(e:Event):void {
                var data:ByteArray = _urlLoader.data as ByteArray;
                _exif = new ExifInfo(data);
                super.loadBytes(data, _context);
                _urlLoader = null;
                dispatchEvent(e);
            }

     

    So my idea was to simply add this in your script, like this, since we dont need Loader anymore because we have already the ByteArray :

     

            private function readMediaData():void
            {
                trace("Getting byteArray");
                var imageBytes:ByteArray = new ByteArray();
                dataSource.readBytes( imageBytes );
               
                trace("Data Trace");
                trace(imageBytes.length);
                imageBytes.position = 0;
                var string:String = imageBytes.readUTFBytes(300);
                trace( string );

     

                // ------------------------------------------------------------------- trying to get exif data

     

                trace("Exif info");           
                var exif:ExifInfo = new ExifInfo(imageBytes);
                ldr = new Loader();
                ldr.loadBytes(imageBytes);
               
                trace("exif =");
                trace(exif);
                trace("exif trace");
                trace(exif.ifds);
                trace(exif.ifds.primary);
                trace(exif.ifds.exif);
                trace(exif.ifds.gps);
                trace(exif.ifds.thumbnail);
               
            }

     

    So when i trace this i get :

     

    Getting byteArray
    Data Trace
    274667
    ÿØÿà

     

    So no problem until here, it is working, now here is the second part

     

    Exif info
    exif =
    [object ExifInfo]
    exif trace
    null
    TypeError: Error #1009: Cannot access a property or method of a null object reference.

     

    So where im blocked is to convert this ByteArray into an ExifInfo object, and then geting the different properties such as orientation, gps etc.

    But it is null ~

     

    I know you told be before that data should be in APP0 and not APP1. But since this library is working perfectly on android and also on local files, i wanted to give it a try.

     

     

    I think solution is really close, its just a question of syntax  ! ^^

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 4, 2011 12:40 AM   in reply to WeLoveKimchi

    I also got errors with every Exif library I tried (posibly because iOS puts in the JFIF APP0 marker and both JFIF and EXIF specs state that their marker goes first). However, this one was able to get the orientation: http://devstage.blogspot.com/2011/02/extract-jpg-exif-metadata-in.html

     

    I used the following code to get the orientation from the exif reader class:

            private function onFileSelect( event:Event ):void
            {
                var stream:FileStream = new FileStream();
                var buffer:ByteArray = new ByteArray();
               
                stream.open( file, FileMode.READ );
                stream.readBytes(buffer,0,8)
                stream.position = 0;
                trace( file.name + " is " + checkMagicNumber( buffer ) );
                stream.close();           
               
                var reader:ExifReader = new ExifReader();
                reader.addEventListener(Event.COMPLETE, function (e:Event):void{
                    // Prints all of the metadata keys parsed.
                    if( reader.hasKey( "Orientation" ) )
                    {
                        var orientation:uint = uint(reader.getValue("Orientation"));
                        trace(orientation.toString(16));
                    }

     

                });
                reader.load(new URLRequest(file.url), true);
            }

     

    1 = normal

    3 = rotated 180 degrees (upside down)

    6 = rotated 90 degrees CW

    8 = rotated 90 degrees CCW

    9 = unknown

     

    Note that I was running this in a desktop AIR app, not on my phone (it was quicker). That's why I'm using the File API here. I tested images uploaded from the phone that had passed through the CameraRoll. So adapting the Exif reader to use Loader.loadBytes() instead of Loader.load() remains to be done for your use case. Still, this should get you started.

     

    (Please note the developer's terms of use if you use the exif reader code.)

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 7, 2011 8:29 AM   in reply to Joe ... Ward

    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.

     
    |
    Mark as:
  • Currently Being Moderated
    Dec 11, 2011 2:48 PM   in reply to Joe ... Ward

    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!

     
    |
    Mark as:
  • Currently Being Moderated
    Dec 11, 2011 5:15 PM   in reply to ErichCervantez

    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[i] != 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;

            }

     

        }

    }

     
    |
    Mark as:
  • Currently Being Moderated
    May 10, 2012 8:51 AM   in reply to Joe ... Ward

    Hi Joe, I tested the class ExifReader on Android and IOS did not work and has someexample application with the mediaPromisse?

     

    thank you

     
    |
    Mark as:
  • Currently Being Moderated
    May 14, 2012 11:47 PM   in reply to nirnirnir

    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.

     
    |
    Mark as:
  • Currently Being Moderated
    May 16, 2012 10:03 AM   in reply to JuniorTrials

    JuniorTrial,

     

    ExifReader isn't something I wrote, so I can't vouch for how robust it is. I know it worked in my initial tests.

     

    Here's something I wrote up about using another library: http://recycledinformation.blogspot.com/2011/08/reading-exif-data-on-i os.html (but again, most of it isn't my code. You might need to do some additional debugging/fixing to get it to work with all possible jpg images).

     
    |
    Mark as:
  • Currently Being Moderated
    May 16, 2012 10:04 AM   in reply to GoonNguyen

    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.)

     
    |
    Mark as:
  • Currently Being Moderated
    May 16, 2012 7:53 PM   in reply to Joe ... Ward

    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!

     
    |
    Mark as:
  • Currently Being Moderated
    Jul 12, 2012 5:59 AM   in reply to Joe ... Ward

    Hi Joe,

     

    Great stuff. Just what I was looking for, as I tried to do a similar scenario: Press a button, snap a picture and upload to a vault. It works great accept for the rotated image on iOs. I understand how to use the ExifInfo to find out if a picture is rotated. But I have a hard time understanding how I actually rotate a picture before uploading to the fault. Could you help me out with that?

     

    Thanks a lot.

     

    -Rogier

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 29, 2012 6:51 AM   in reply to GoonNguyen

    i get to here

     

    var data:ByteArray = new ByteArray();

              dataSource.readBytes( data );

              exif = new ExifInfo(data);

     

    and when i try to instantiate the ExifInfo i get this error..

     

    exception fault: Error: Error #2030: End of file was encountered.

        at flash.utils::ByteArray/readUnsignedByte()

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 29, 2012 10:54 AM   in reply to rogierdoekes

    @rogierdoekes,

     

    (I can't claim to be an expert on this topic, but...) I think there are two ways to rotate the image before uploading it.

     

    One is to rewrite the file data transposing the pixel rows and columns and setting other values in the jpeg format as needed. This is probably the fastest computationally, but would require a strong understanding of the jpeg file format on your part.

     

    The other way I can think of is to render the image to a Flash bitmap in the desired orientation, reencode as a jpeg and upload that. Easier to program, but slower computationally, and you add a generation of quality loss to the jpeg by reencoding.

     

    @bonsai123,

    When reading binary data it is really easy to make mistakes and go past the end. Perhaps the code is looking for a particular byte sequence and not finding it or the data itself is malformed. I can't debug it from here. You'll need to understand what the code is trying to do and trace the execution to see where it is going wrong.

     

    -Joe

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 29, 2012 3:44 PM   in reply to Joe ... Ward

    thanks for the reply Joe.. here's my code using Air 3.4 compiling .ipa file using ipa-debug-interpreter

     

    public function CameraView() : void

            {

                trace("CameraView.as -> CameraView");

     

                //

                // init camera

                _deviceCam = new CameraUI();

     

                if (CameraUI.isSupported)

                {

                    _deviceCam.addEventListener(MediaEvent.COMPLETE, imageCaptured);

                    _deviceCam.addEventListener(flash.events.Event.CANCEL, captureCanceled);

                    _deviceCam.addEventListener(ErrorEvent.ERROR, errorEvent);

                    _deviceCam.launch(MediaType.IMAGE);

                }

                else

                    trace("CameraUI not supported");

            }

     

            private function imageCaptured(event : MediaEvent) : void

            {

                trace("CameraView.as -> imageCaptured");

     

                _mediaPromise = event.data;

     

                _dataSource = _mediaPromise.open();

     

                if (_mediaPromise.isAsync)

                {

                    trace("Asynchronous media promise.");

     

                    var eventSource : IEventDispatcher = _dataSource as IEventDispatcher;

     

                    eventSource.addEventListener(flash.events.Event.COMPLETE, onMediaLoaded);

                }

                else

                {

                    trace("Synchronous media promise.");

     

                    readMediaData();

                }

            }

     

            private function onMediaLoaded(event : flash.events.Event) : void

            {

                trace("Media load complete");

     

                readMediaData();

            }

     

            private function readMediaData() : void

            {

                // THIS PART IS READING JFIF DATA:

     

                var data : ByteArray = new ByteArray();

     

                _dataSource.readBytes(data);

     

                _exif = new ExifInfo(data);

     

                trace("CameraView.as -> readMediaData " + _exif);

     

                // exception fault: Error: Error #2030: End of file was encountered.

               // at flash.utils::ByteArray/readUnsignedByte()

            }

     

     

    UPDATE :

     

    actually now for some reason i'm not getting that error anymore but am getting a null object reference by replacing the last trace above with this..

     

    trace("CameraView.as -> readMediaData " + _exif.ifds.primary);

     

    another update :

     

    i hadn't correctly updated the ExifInfo.as file

     

      if (!hasSoiMaker(stream) ) {

                    return false;

                }

     

    was still..

     

                if (!hasSoiMaker(stream) || !hasAPP1Maker(stream)) {

                    return false;

                }

     

     

    it's working now!

     

    thanks again for the reply Joe

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 29, 2012 5:00 PM   in reply to Joe ... Ward

    Hi Joe,

     

    Do you have an example how to render the image to a Flash bitmap in the desired orientation? I have a hard time any scripts that does that. Pardon me my ignorance it is indeed trivial.

     

    Thanks much for your anwer,

     

    -Rogier

     
    |
    Mark as:
  • Currently Being Moderated
    Aug 29, 2012 6:03 PM   in reply to Joe ... Ward

    actually.. one last thing. now i know the orientation of the image i need to display it

     

    _imageLoader = new Loader();

                    _imageLoader.loadBytes(data);

     

                    trace("loader width = " + _imageLoader.width); // this traces 0

     

    and so..

     

     

    var bmd : BitmapData = new BitmapData(_imageLoader.width, _imageLoader.height);

                bmd.draw(_imageLoader);

     

    gives an invalid bitmapdata error

     

    any ideas?

     

    cheers

     

    UPDATE :

     

    found something.. needs a slight delay for some reason

     

    _imageLoader.loadBytes(data);

    setTimeout(function(){

       trace("loader width = " + _imageLoader.width); // 960

    },500)

     
    |
    Mark as:
  • Currently Being Moderated
    Dec 9, 2012 8:15 PM   in reply to bonsai123

    You should add this:

     

    _imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadComplete);

     

    function onLoadComplete(e:Event)
    {

          trace("loader width = " + _imageLoader.width);

    }

     

    Please, don't use setTimeout for this situation, very dangerous if you're using the low performance device to test.

     
    |
    Mark as:
  • Currently Being Moderated
    Jul 7, 2013 12:42 AM   in reply to GoonNguyen

    hi GoonNguyen

     

    I tried your code and modify the jp.shichiseki librariby but got error like this:

     

    TypeError: Error #1034: Type Coercion failed: cannot convert "<tags level="0th IFD TIFF Tags">

      <tag tag_name="Image width" field_name="ImageWidth" id="0x0100" compressed="J">

        <uncompressed chunky="M" planar="M" ycc="M" />

      </tag>

      <tag tag_name="Image height" field_name="ImageLength" id="0x0101" compressed="J">

        <uncompressed chunky="M" planar="M" ycc="M" />

      </tag>

    ......

     


    any sugesstion for this?  I think that is caused by jp.shichiseki librariby that I modified.

     

    Thank you.

     

     

    <*Update*>

     

    sorry I just quick searching and find the answer of this:

     

    http://forums.adobe.com/thread/1170567

     

    "Open jp.shichiseki.exif.Tags.as, and replace line 52 by this :

     

    return XML( levels[level].data );

     

    It forces getSet() function to return a full typed XML, and not just a String formatted as an XML."

     

     

    It's works now !

    I still put here in case someone make the same mistake like me.

     
    |
    Mark as:

More Like This

  • Retrieving data ...

Bookmarked By (1)

Answers + Points = Status

  • 10 points awarded for Correct Answers
  • 5 points awarded for Helpful Answers
  • 10,000+ points
  • 1,001-10,000 points
  • 501-1,000 points
  • 5-500 points