Skip to content

Instantly share code, notes, and snippets.

@runeb
Created May 23, 2014 10:49
Show Gist options
  • 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>
@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