Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Auto-rotate images locally in the browser by parsing exif data
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<input id="file" type="file" accept="image/*" />
<br/>
<h2>As read:</h2>
<img id="placeholder1" width=300/><br/>
<h2>Rotated by exif data:</h2>
<img id="placeholder2" width=300/>
<script>
// Exif orientation value to css transform mapping
// Does not include flipped orientations
var rotation = {
1: 'rotate(0deg)',
3: 'rotate(180deg)',
6: 'rotate(90deg)',
8: 'rotate(270deg)'
};
function _arrayBufferToBase64( buffer ) {
var binary = ''
var bytes = new Uint8Array( buffer )
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] )
}
return window.btoa( binary );
}
var orientation = function(file, callback) {
var fileReader = new FileReader();
fileReader.onloadend = function() {
var base64img = "data:"+file.type+";base64," + _arrayBufferToBase64(fileReader.result);
var scanner = new DataView(fileReader.result);
var idx = 0;
var value = 1; // Non-rotated is the default
if(fileReader.result.length < 2 || scanner.getUint16(idx) != 0xFFD8) {
// Not a JPEG
if(callback) {
callback(base64img, value);
}
return;
}
idx += 2;
var maxBytes = scanner.byteLength;
while(idx < maxBytes - 2) {
var uint16 = scanner.getUint16(idx);
idx += 2;
switch(uint16) {
case 0xFFE1: // Start of EXIF
var exifLength = scanner.getUint16(idx);
maxBytes = exifLength - idx;
idx += 2;
break;
case 0x0112: // Orientation tag
// Read the value, its 6 bytes further out
// See page 102 at the following URL
// http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf
value = scanner.getUint16(idx + 6, false);
maxBytes = 0; // Stop scanning
break;
}
}
if(callback) {
callback(base64img, value);
}
}
fileReader.readAsArrayBuffer(file);
};
$(function() {
$('#file').change(function() {
var file = $(this)[0].files[0];
if(file) {
orientation(file, function(base64img, value) {
$('#placeholder1').attr('src', base64img);
console.log(rotation[value]);
var rotated = $('#placeholder2').attr('src', base64img);
if(value) {
rotated.css('transform', rotation[value]);
}
});
}
});
});
</script>
@orotemo

This comment has been minimized.

Copy link

@orotemo orotemo commented Jan 24, 2017

as surprising as it is, still relevant :) here is a fiddle for peoples' convenience

@sarnobat

This comment has been minimized.

Copy link

@sarnobat sarnobat commented Apr 4, 2017

How can I hook this into an "" tag? I don't want to upload the file.

@thinklinux

This comment has been minimized.

Copy link

@thinklinux thinklinux commented Jun 13, 2017

@orotemo Thanks! It's still a valid problem and it's not supported by all browsers.

@vdurmont

This comment has been minimized.

Copy link

@vdurmont vdurmont commented Aug 1, 2017

Thanks @orotemo

@Snesi

This comment has been minimized.

Copy link

@Snesi Snesi commented Sep 29, 2017

Thank you @otoremo!

For those of you using Angular2+. I made an angular pipe to use this code

https://gist.github.com/Snesi/a20a0fd298c6c32598644353edf2eb7f

@tamihelcich

This comment has been minimized.

Copy link

@tamihelcich tamihelcich commented Jan 3, 2018

I tried the fiddle for this: the image I am using is indeed a jpeg image with exif data from my computer after being pulled from the camera. The image did not get rotated. I am getting the results of value 1 in the console, so I'm thinking it doesn't know its a jpeg, or maybe something else?

@geekorlife

This comment has been minimized.

Copy link

@geekorlife geekorlife commented Feb 1, 2018

@nicolassaad

This comment has been minimized.

Copy link

@nicolassaad nicolassaad commented Mar 2, 2018

I'm having the same issue as tamihelcich the fiddle doesn't seem to work at all.

@simonjackson

This comment has been minimized.

Copy link

@simonjackson simonjackson commented Mar 6, 2018

The code above worked for some images but not others. I tracked it down to the byte ordering
and added in a check for the 'endian-ness' of the Exif data.

var orientation = function (file, callback) {
    var fileReader = new FileReader();
    fileReader.onloadend = function () {
        var base64img = "data:" + file.type + ";base64," + _arrayBufferToBase64(fileReader.result);
        var scanner = new DataView(fileReader.result);
        var idx = 0;
        var value = 1; // Non-rotated is the default
        if (fileReader.result.length < 2 || scanner.getUint16(idx) != 0xFFD8) {
            // Not a JPEG
            if (callback) {
                callback(base64img, value);
            }
            return;
        }
        idx += 2;
        var maxBytes = scanner.byteLength;
        var littleEndian = false;
        while (idx < maxBytes - 2) {
            var uint16 = scanner.getUint16(idx, littleEndian);
            idx += 2;
            switch (uint16) {
                case 0xFFE1: // Start of EXIF
                    var endianNess = scanner.getUint16(idx + 8);
                    // II (0x4949) Indicates Intel format - Little Endian
                    // MM (0x4D4D) Indicates Motorola format - Big Endian
                    if (endianNess === 0x4949) {
                        littleEndian = true;
                    }
                    var exifLength = scanner.getUint16(idx, littleEndian);
                    maxBytes = exifLength - idx;
                    idx += 2;
                    break;
                case 0x0112: // Orientation tag
                    // Read the value, its 6 bytes further out
                    // See page 102 at the following URL
                    // http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf
                    value = scanner.getUint16(idx + 6, littleEndian);
                    maxBytes = 0; // Stop scanning
                    break;
            }
        }
        if (callback) {
            callback(base64img, value);
        }
    }
    fileReader.readAsArrayBuffer(file);
};
@kedrovski

This comment has been minimized.

Copy link

@kedrovski kedrovski commented Mar 11, 2019

The code above worked for some images but not others. I tracked it down to the byte ordering
and added in a check for the 'endian-ness' of the Exif data.

var orientation = function (file, callback) {
    var fileReader = new FileReader();
    fileReader.onloadend = function () {
        var base64img = "data:" + file.type + ";base64," + _arrayBufferToBase64(fileReader.result);
        var scanner = new DataView(fileReader.result);
        var idx = 0;
        var value = 1; // Non-rotated is the default
        if (fileReader.result.length < 2 || scanner.getUint16(idx) != 0xFFD8) {
            // Not a JPEG
            if (callback) {
                callback(base64img, value);
            }
            return;
        }
        idx += 2;
        var maxBytes = scanner.byteLength;
        var littleEndian = false;
        while (idx < maxBytes - 2) {
            var uint16 = scanner.getUint16(idx, littleEndian);
            idx += 2;
            switch (uint16) {
                case 0xFFE1: // Start of EXIF
                    var endianNess = scanner.getUint16(idx + 8);
                    // II (0x4949) Indicates Intel format - Little Endian
                    // MM (0x4D4D) Indicates Motorola format - Big Endian
                    if (endianNess === 0x4949) {
                        littleEndian = true;
                    }
                    var exifLength = scanner.getUint16(idx, littleEndian);
                    maxBytes = exifLength - idx;
                    idx += 2;
                    break;
                case 0x0112: // Orientation tag
                    // Read the value, its 6 bytes further out
                    // See page 102 at the following URL
                    // http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf
                    value = scanner.getUint16(idx + 6, littleEndian);
                    maxBytes = 0; // Stop scanning
                    break;
            }
        }
        if (callback) {
            callback(base64img, value);
        }
    }
    fileReader.readAsArrayBuffer(file);
};

thanks to author and you

@philipp-spiess

This comment has been minimized.

Copy link

@philipp-spiess philipp-spiess commented Jul 15, 2019

There is another issue in line 53 of the original implementation:

          maxBytes = exifLength - idx;

maxBytes is relative to the whole file and idx is the offset within this file, where we detected the beginning of the EXIF header. We need to add the exifLength to the current offset to get the proper end:

          maxBytes = exifLength + idx;

@tamihelcich / @nicolassaad Any chance you still have the images you were trying so I can test if this fixes the issue? 🙂

@vdavid

This comment has been minimized.

Copy link

@vdavid vdavid commented Apr 10, 2020

Thanks a lot for this gist @runeb and for the fixes @kedrovski! I used both as the base for a new, class-based version.

Improvements:

  • Replaced the callback with async/await
  • Handles all 8 possible orientations
  • Made the base64 part optional (it might not be needed for everyone—I didn't need it).
  • Can return the raw orientation value (1–8)
  • Replaced the loop with a functional programming-based solution
  • I broke up the code to smaller methods to make it a bit more documented and easier to modify.

Here you go, I hope someone finds it useful: https://gist.github.com/vdavid/3f9b66b60f52204317a4cc0e77097913

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment