Skip to content

Instantly share code, notes, and snippets.

@runeb
Created May 23, 2014 10:49
Show Gist options
  • Star 89 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save runeb/c11f864cd7ead969a5f0 to your computer and use it in GitHub Desktop.
Save runeb/c11f864cd7ead969a5f0 to your computer and use it in GitHub Desktop.
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
Copy link

orotemo commented Jan 24, 2017

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

@sarnobat
Copy link

sarnobat commented Apr 4, 2017

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

@thinklinux
Copy link

thinklinux commented Jun 13, 2017

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

@vdurmont
Copy link

vdurmont commented Aug 1, 2017

Thanks @orotemo

@Snesi
Copy link

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
Copy link

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
Copy link

@nicolassaad
Copy link

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

@simonjackson
Copy link

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
Copy link

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
Copy link

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
Copy link

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