Accessing JPEG EXIF rotation data in JavaScript on the client side
Asked Answered
N

8

141

I'd like to rotate photos based on their original rotation, as set by the camera in JPEG EXIF image data. The trick is that all this should happen in the browser, using JavaScript and <canvas>.

How could JavaScript access JPEG, a local file API object, local <img> or remote <img>, EXIF data to read the rotation information?

Server-side answers are not OK; I am looking for a client-side solution.

Nefarious answered 28/9, 2011 at 14:25 Comment(0)
O
292

If you only want the orientation tag and nothing else and don't like to include another huge javascript library I wrote a little code that extracts the orientation tag as fast as possible (It uses DataView and readAsArrayBuffer which are available in IE10+, but you can write your own data reader for older browsers):

function getOrientation(file, callback) {
    var reader = new FileReader();
    reader.onload = function(e) {

        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8)
        {
            return callback(-2);
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) 
        {
            if (view.getUint16(offset+2, false) <= 8) return callback(-1);
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) 
            {
                if (view.getUint32(offset += 2, false) != 0x45786966) 
                {
                    return callback(-1);
                }

                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;
                for (var i = 0; i < tags; i++)
                {
                    if (view.getUint16(offset + (i * 12), little) == 0x0112)
                    {
                        return callback(view.getUint16(offset + (i * 12) + 8, little));
                    }
                }
            }
            else if ((marker & 0xFF00) != 0xFF00)
            {
                break;
            }
            else
            { 
                offset += view.getUint16(offset, false);
            }
        }
        return callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

// usage:
var input = document.getElementById('input');
input.onchange = function(e) {
    getOrientation(input.files[0], function(orientation) {
        alert('orientation: ' + orientation);
    });
}
<input id='input' type='file' />

values:

-2: not jpeg
-1: not defined

enter image description here

For those using Typescript, you can use the following code:

export const getOrientation = (file: File, callback: Function) => {
  var reader = new FileReader();

  reader.onload = (event: ProgressEvent) => {

    if (! event.target) {
      return;
    }

    const file = event.target as FileReader;
    const view = new DataView(file.result as ArrayBuffer);

    if (view.getUint16(0, false) != 0xFFD8) {
        return callback(-2);
    }

    const length = view.byteLength
    let offset = 2;

    while (offset < length)
    {
        if (view.getUint16(offset+2, false) <= 8) return callback(-1);
        let marker = view.getUint16(offset, false);
        offset += 2;

        if (marker == 0xFFE1) {
          if (view.getUint32(offset += 2, false) != 0x45786966) {
            return callback(-1);
          }

          let little = view.getUint16(offset += 6, false) == 0x4949;
          offset += view.getUint32(offset + 4, little);
          let tags = view.getUint16(offset, little);
          offset += 2;
          for (let i = 0; i < tags; i++) {
            if (view.getUint16(offset + (i * 12), little) == 0x0112) {
              return callback(view.getUint16(offset + (i * 12) + 8, little));
            }
          }
        } else if ((marker & 0xFF00) != 0xFF00) {
            break;
        }
        else {
            offset += view.getUint16(offset, false);
        }
    }
    return callback(-1);
  };

  reader.readAsArrayBuffer(file);
}
Ogletree answered 9/9, 2015 at 23:18 Comment(21)
for 2,4,5,7 to get correct image you need to rotate and flip, right?Chalcedony
The orientation of my image is 3..How do i set the orientation to 1??Deerskin
I tried using a few jpg's and most of them returned -1, regardless of their orientation. Does it mean it can't acces the EXIF data?Responsiveness
@Responsiveness I believe it means, I checked different jpeg from different phones and it worked, you can use a hex reader to check how EXIF data is stored, or send me one of the so I can refine the code!Ogletree
@Ogletree Sure, here is one of them: i.imgur.com/gOnS43q.jpg EDIT: Better yet, I took one with my phone (iPhone 6) just a minute ago in portrait mode, it returned -1. i.imgur.com/MCxV975.jpgResponsiveness
@Responsiveness I checked the image in photoshop, actually it has no EXIF data, I believe services like imgur remove EXIF data from images.Ogletree
What about png / gif?Lyric
@Lyric PNG or GIF don't have any standard format to store image orientation #9542859Ogletree
@Ogletree Very nice. Is there a reason why you specifically chose 64 * 1024 for the second parameter to slice?Theretofore
@HoagyCarmichael The exif data wont be after the first 64KB of file data, it can even be less or you can pass the whole file.Ogletree
Working for me, but I needed to change the last line to just reader.readAsArrayBuffer(file); without the slice as I intend to use the buffer for my base64 image, otherwise, you'll just see the first slice of the image. BTW, this is not required if you just need the orientation information. ThanksLateshalatest
@PhilipMurphy How exactly did you extend this to get other parts of EXIF data? I don't see any slice in the source, I want to use it to get flash dataCrean
@DaraJava I removed the slice part because sometimes the tag came in after the limit, but it will slow the operation if the tag is never found. Anyway, unlike orientation tag, Flash tag is not in the IFD0 directory and my code only search this part. to get Flash tag you must search SubIFD directory. You can find a good tutorial on EXIF here: media.mit.edu/pia/Research/deepview/exif.htmlOgletree
@Ali: is it critical to read the whole file, as you do in reader.readAsArrayBuffer(file);? Or can we settle for reading lesser bytes? I ask because I'd want the solution to be independent of size of file selected by user. Afterall, we solely need the orientation tag, nothing more.Ultramicroscopic
@HassanBaig, no, the beginning is enough, I used slice before, but due larger headers I removed it.Ogletree
In some cases the line ar tags = view.getUint16(offset, little); throws RangeError: argument 1 accesses an index that is out of range. For instance, try this image: user-images.githubusercontent.com/18588945/… For answer completeness, what work-around would you suggest?Ultramicroscopic
@HassanBaig I gave that image a try and received -1 (no Orientation data / not defined). When I checked it out with exiftool there is exif data there but not much, and it's missing the Orientation entirelyArise
How would you do this with an <img> tag?Oxbridge
@Ogletree This used to include a slice for performance reasons but this was removed due to "large headers". What is meant by large headers? I'm keen to include slice if it's more performant, but I would like to understand the implications of doing so.Tirrell
To answer my own question: Exif should appear in the first 64 KB, however this is not guaranteed—it may be possible for the Exif to extend beyond (#3249446). Therefore we have to read the whole file for safety. Alternatively, instead of reading the file as an ArrayBuffer, we could use streams (where supported) so we lazily read the file until the Exif has been found: mobile.twitter.com/jaffathecake/status/1085443592678752256. If anyone has success with this approach, please share!Tirrell
Thanks for the help, Whenever I take pictures from a phone camera, it is alerting me orientation is 6, I want my photos to come in the orientation as they were taken in i.e. if I take a photo in portrait, then it stays in a portrait and if I take a photo in landscape then it stays in a landscape, currently, when I take in portrait, it alerts orientation as 6 and then it rotates it to landscape, how to prevent this auto rotation and keep the original orientation as it was captured in, please let me know. Thanks again, Great work.Schnook
A
24

You can use the exif-js library in combination with the HTML5 File API: http://jsfiddle.net/xQnMd/1/.

$("input").change(function() {
    var file = this.files[0];  // file
        fr   = new FileReader; // to read file contents

    fr.onloadend = function() {
        // get EXIF data
        var exif = EXIF.readFromBinaryFile(new BinaryFile(this.result));

        // alert a value
        alert(exif.Make);
    };

    fr.readAsBinaryString(file); // read the file
});
Albanese answered 28/9, 2011 at 15:1 Comment(8)
Thanks. The JS lib in the question looks little bit outdated, but would probably work.Nefarious
See also my demo of a file upload widget I just wrote. It uses the EXIF.js library mentioned above to read the EXIF orientation flag in the image file’s metatdata. Based on the information, it applies the rotation using a canvas element... sandbox.juurlink.org/html5imageuploaderSuccessful
Attempting to even include binaryajax.js in my project causes an access denied error.Kuykendall
Where does the EXIF object come from? The BinaryFile script does not seem to contain it, and as far as I can tell, it isn't part of jquery or any other script I regularly use...Praetorian
@Praetorian There's 2 js files you need to add. Check the jsfiddle link in the answer: nihilogic.dk/labs/exif/exif.js + nihilogic.dk/labs/binaryajax/binaryajax.jsDarter
Thanks! That's what I needed...I eventually found the exif.js file.Praetorian
The library website seems down, and the only other ExifReader libraries I have found were limited in browser support. Is there any good alternative?Rustle
Binaryajax.js can be found here, github.com/jseidelin/binaryajax/blob/master/binaryajax.jsDeeplaid
K
20

Firefox 26 supports image-orientation: from-image: images are displayed portrait or landscape, depending on EXIF data. (See sethfowler.org/blog/2013/09/13/new-in-firefox-26-css-image-orientation.)

There is also a bug to implement this in Chrome.

Beware that this property is only supported by Firefox and is likely to be deprecated.

Katha answered 20/9, 2013 at 9:9 Comment(3)
Thanks for the link to the bug report. I starred it so that the Chrome team knows more people want this.Darter
According to this comment bugs.chromium.org/p/chromium/issues/detail?id=158753#c104 by a Chromium project member: "The change is in Chrome 81. That will roll out to the public as the Stable version in 8-10 week's time"Metabolic
Implemented on Chrome starting with 81 🎉 It will take a while before people update their browser though - keep an eye on caniuseNerva
F
11

https://github.com/blueimp/JavaScript-Load-Image is a modern javascript library that can not only extract the exif orientation flag - it can also correctly mirror/rotate JPEG images on the client side.

I just solved the same problem with this library: JS Client-Side Exif Orientation: Rotate and Mirror JPEG Images

Frailty answered 15/12, 2013 at 22:47 Comment(0)
C
6

I upload expansion code to show photo by android camera on html as normal on some img tag with right rotaion, especially for img tag whose width is wider than height. I know this code is ugly but you don't need to install any other packages. (I used above code to obtain exif rotation value, Thank you.)

function getOrientation(file, callback) {
  var reader = new FileReader();
  reader.onload = function(e) {

    var view = new DataView(e.target.result);
    if (view.getUint16(0, false) != 0xFFD8) return callback(-2);
    var length = view.byteLength, offset = 2;
    while (offset < length) {
      var marker = view.getUint16(offset, false);
      offset += 2;
      if (marker == 0xFFE1) {
        if (view.getUint32(offset += 2, false) != 0x45786966) return callback(-1);
        var little = view.getUint16(offset += 6, false) == 0x4949;
        offset += view.getUint32(offset + 4, little);
        var tags = view.getUint16(offset, little);
        offset += 2;
        for (var i = 0; i < tags; i++)
          if (view.getUint16(offset + (i * 12), little) == 0x0112)
            return callback(view.getUint16(offset + (i * 12) + 8, little));
      }
      else if ((marker & 0xFF00) != 0xFF00) break;
      else offset += view.getUint16(offset, false);
    }
    return callback(-1);
  };
  reader.readAsArrayBuffer(file);
}

var isChanged = false;
function rotate(elem, orientation) {
    if (isIPhone()) return;

    var degree = 0;
    switch (orientation) {
        case 1:
            degree = 0;
            break;
        case 2:
            degree = 0;
            break;
        case 3:
            degree = 180;
            break;
        case 4:
            degree = 180;
            break;
        case 5:
            degree = 90;
            break;
        case 6:
            degree = 90;
            break;
        case 7:
            degree = 270;
            break;
        case 8:
            degree = 270;
            break;
    }
    $(elem).css('transform', 'rotate('+ degree +'deg)')
    if(degree == 90 || degree == 270) {
        if (!isChanged) {
            changeWidthAndHeight(elem)
            isChanged = true
        }
    } else if ($(elem).css('height') > $(elem).css('width')) {
        if (!isChanged) {
            changeWidthAndHeightWithOutMargin(elem)
            isChanged = true
        } else if(degree == 180 || degree == 0) {
            changeWidthAndHeightWithOutMargin(elem)
            if (!isChanged)
                isChanged = true
            else
                isChanged = false
        }
    }
}


function changeWidthAndHeight(elem){
    var e = $(elem)
    var width = e.css('width')
    var height = e.css('height')
    e.css('width', height)
    e.css('height', width)
    e.css('margin-top', ((getPxInt(height) - getPxInt(width))/2).toString() + 'px')
    e.css('margin-left', ((getPxInt(width) - getPxInt(height))/2).toString() + 'px')
}

function changeWidthAndHeightWithOutMargin(elem){
    var e = $(elem)
    var width = e.css('width')
    var height = e.css('height')
    e.css('width', height)
    e.css('height', width)
    e.css('margin-top', '0')
    e.css('margin-left', '0')
}

function getPxInt(pxValue) {
    return parseInt(pxValue.trim("px"))
}

function isIPhone(){
    return (
        (navigator.platform.indexOf("iPhone") != -1) ||
        (navigator.platform.indexOf("iPod") != -1)
    );
}

and then use such as

$("#banner-img").change(function () {
    var reader = new FileReader();
    getOrientation(this.files[0], function(orientation) {
        rotate($('#banner-img-preview'), orientation, 1)
    });

    reader.onload = function (e) {
        $('#banner-img-preview').attr('src', e.target.result)
        $('#banner-img-preview').css('display', 'inherit')

    };

    // read the image file as a data URL.
    reader.readAsDataURL(this.files[0]);

});
Capriccio answered 4/4, 2017 at 8:30 Comment(0)
O
5

Improving / Adding more functionality to Ali's answer from earlier, I created a util method in Typescript that suited my needs for this issue. This version returns rotation in degrees that you might also need for your project.

ImageUtils.ts

/**
 * Based on StackOverflow answer: https://stackoverflow.com/a/32490603
 *
 * @param imageFile The image file to inspect
 * @param onRotationFound callback when the rotation is discovered. Will return 0 if if it fails, otherwise 0, 90, 180, or 270
 */
export function getOrientation(imageFile: File, onRotationFound: (rotationInDegrees: number) => void) {
  const reader = new FileReader();
  reader.onload = (event: ProgressEvent) => {
    if (!event.target) {
      return;
    }

    const innerFile = event.target as FileReader;
    const view = new DataView(innerFile.result as ArrayBuffer);

    if (view.getUint16(0, false) !== 0xffd8) {
      return onRotationFound(convertRotationToDegrees(-2));
    }

    const length = view.byteLength;
    let offset = 2;

    while (offset < length) {
      if (view.getUint16(offset + 2, false) <= 8) {
        return onRotationFound(convertRotationToDegrees(-1));
      }
      const marker = view.getUint16(offset, false);
      offset += 2;

      if (marker === 0xffe1) {
        if (view.getUint32((offset += 2), false) !== 0x45786966) {
          return onRotationFound(convertRotationToDegrees(-1));
        }

        const little = view.getUint16((offset += 6), false) === 0x4949;
        offset += view.getUint32(offset + 4, little);
        const tags = view.getUint16(offset, little);
        offset += 2;
        for (let i = 0; i < tags; i++) {
          if (view.getUint16(offset + i * 12, little) === 0x0112) {
            return onRotationFound(convertRotationToDegrees(view.getUint16(offset + i * 12 + 8, little)));
          }
        }
        // tslint:disable-next-line:no-bitwise
      } else if ((marker & 0xff00) !== 0xff00) {
        break;
      } else {
        offset += view.getUint16(offset, false);
      }
    }
    return onRotationFound(convertRotationToDegrees(-1));
  };
  reader.readAsArrayBuffer(imageFile);
}

/**
 * Based off snippet here: https://github.com/mosch/react-avatar-editor/issues/123#issuecomment-354896008
 * @param rotation converts the int into a degrees rotation.
 */
function convertRotationToDegrees(rotation: number): number {
  let rotationInDegrees = 0;
  switch (rotation) {
    case 8:
      rotationInDegrees = 270;
      break;
    case 6:
      rotationInDegrees = 90;
      break;
    case 3:
      rotationInDegrees = 180;
      break;
    default:
      rotationInDegrees = 0;
  }
  return rotationInDegrees;
}

Usage:

import { getOrientation } from './ImageUtils';
...
onDrop = (pics: any) => {
  getOrientation(pics[0], rotationInDegrees => {
    this.setState({ image: pics[0], rotate: rotationInDegrees });
  });
};
Obeded answered 26/10, 2018 at 21:14 Comment(1)
Hi, this helps a lot, Thanks, but when I take a photo from an android phone, it gives me an orientation as 6 and when I take a photo from iPhone, it gives me an orientation as 3, how to change this such that my photo orientation does not rotate at all and stays in the same orientation as it was captured in. Please help me with that. Basically, since orientation 6 is 90 degrees (as mentioned in your code), all I want to do is to make it 0 degrees but how to do that, I am unaware of that.Schnook
H
4

If you want it cross-browser, your best bet is to do it on the server. You could have an API that takes a file URL and returns you the EXIF data; PHP has a module for that.

This could be done using Ajax so it would be seamless to the user. If you don't care about cross-browser compatibility, and can rely on HTML5 file functionality, look into the library JsJPEGmeta that will allow you to get that data in native JavaScript.

Highbred answered 28/9, 2011 at 14:28 Comment(12)
The last part of this article explains how the library works.Highbred
Thank's. The JS script looks sweet. I am not using PHP (in fact I hate it) and I was looking for pure client side Javascript solution.Nefarious
Here is a reference implementation github.com/miohtama/Krusovice/blob/master/src/tools/resizer.jsNefarious
@MikkoOhtamaa: You need to understand that Stack Overflow answers questions for everybody, just just the original person asking it. The next person who has the same objective as you may be a PHP developer - why would you want to deny them the information that Xeon06 included? It was inappropriate to edit that out, just because you don't want a PHP solution.Hax
The question says "in Javascript" so the part was irrelevant. There are many other similar questions and answers for PHP already on the site and it is unnecessary noise regarding this question.Nefarious
If people ask for Javascript solution they don't want to see PHP solution as the first post.Nefarious
I'll add PHP part here for the comments for the sake of those poor PHP programming bastards: php.net/manual/en/book.exif.phpNefarious
@MikkoOhtamaa it would seem like most disagree with you meta.stackexchange.com/questions/157338/… You seem to have some wrongful sense of ownership on the answers to your questions.Highbred
I edited the answer to have the correct answer at the beginning. Sorry for the fuzz.Nefarious
I tried using the jsJPEGmeta.js library and it does not seem to work for me. When trying to view the orientation of a camera image from an iPhone it gives the result as undefined when I know the orientation flag is set.Kuykendall
@ObiWan I wouldn't be sure what the problem is, I would try contacting the authors of the library or making a new question here on Stack Overflow.Highbred
@Alex Turpin, Seems I need you help. Look at this : #45650965Caught
K
3

Check out a module I've written (you can use it in browser) which converts exif orientation to CSS transform: https://github.com/Sobesednik/exif2css

There is also this node program to generate JPEG fixtures with all orientations: https://github.com/Sobesednik/generate-exif-fixtures

Kannada answered 6/1, 2017 at 17:43 Comment(3)
Nice module! However, how does it get EXIF information out from JPEG in the first place?Nefarious
@MikkoOhtamaa thanks and nah it does not, you have to do it with exif-js or exiftool server-sideKannada
This is useful. But it seems to me like it only works correctly for portrait photos, not landscape ones.Refinery

© 2022 - 2024 — McMap. All rights reserved.