JS Client-Side Exif Orientation: Rotate and Mirror JPEG Images

Viewed 191496

Digital camera photos are often saved as JPEG with an EXIF "orientation" tag. To display correctly, images need to be rotated/mirrored depending on which orientation is set, but browsers ignore this information rendering the image. Even in large commercial web apps, support for EXIF orientation can be spotty 1. The same source also provides a nice summary of the 8 different orientations a JPEG can have:

Summary of EXIF Orientations

Sample images are available at 4.

The question is how to rotate/mirror the image on the client side so that it displays correctly and can be further processed if necessary?

There are JS libraries available to parse EXIF data, including the orientation attribute 2. Flickr noted possible performance problem when parsing large images, requiring use of webworkers 3.

Console tools can correctly re-orient the images 5. A PHP script solving the problem is available at 6

14 Answers

For those who have a file from an input control, don't know what its orientation is, are a bit lazy and don't want to include a large library below is the code provided by @WunderBart melded with the answer he links to (https://stackoverflow.com/a/32490603) that finds the orientation.

function getDataUrl(file, callback2) {
        var callback = function (srcOrientation) {
            var reader2 = new FileReader();
            reader2.onload = function (e) {
                var srcBase64 = e.target.result;
                var img = new Image();

                img.onload = function () {
                    var width = img.width,
                        height = img.height,
                        canvas = document.createElement('canvas'),
                        ctx = canvas.getContext("2d");

                    // set proper canvas dimensions before transform & export
                    if (4 < srcOrientation && srcOrientation < 9) {
                        canvas.width = height;
                        canvas.height = width;
                    } else {
                        canvas.width = width;
                        canvas.height = height;
                    }

                    // transform context before drawing image
                    switch (srcOrientation) {
                        case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
                        case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
                        case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
                        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
                        case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
                        case 7: ctx.transform(0, -1, -1, 0, height, width); break;
                        case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
                        default: break;
                    }

                    // draw image
                    ctx.drawImage(img, 0, 0);

                    // export base64
                    callback2(canvas.toDataURL());
                };

                img.src = srcBase64;
            }

            reader2.readAsDataURL(file);
        }

        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);
    }

which can easily be called like such

getDataUrl(input.files[0], function (imgBase64) {
      vm.user.BioPhoto = imgBase64;
});

WunderBart's answer was the best for me. Note that you can speed it up a lot if your images are often the right way around, simply by testing the orientation first and bypassing the rest of the code if no rotation is required.

Putting all of the info from wunderbart together, something like this;

var handleTakePhoto = function () {
    let fileInput: HTMLInputElement = <HTMLInputElement>document.getElementById('photoInput');
    fileInput.addEventListener('change', (e: any) => handleInputUpdated(fileInput, e.target.files));
    fileInput.click();
}

var handleInputUpdated = function (fileInput: HTMLInputElement, fileList) {
    let file = null;

    if (fileList.length > 0 && fileList[0].type.match(/^image\//)) {
        isLoading(true);
        file = fileList[0];
        getOrientation(file, function (orientation) {
            if (orientation == 1) {
                imageBinary(URL.createObjectURL(file));
                isLoading(false);
            }
            else 
            {
                resetOrientation(URL.createObjectURL(file), orientation, function (resetBase64Image) {
                    imageBinary(resetBase64Image);
                    isLoading(false);
                });
            }
        });
    }

    fileInput.removeEventListener('change');
}


// from http://stackoverflow.com/a/32490603
export function getOrientation(file, callback) {
    var reader = new FileReader();

    reader.onload = function (event: any) {
        var view = new DataView(event.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.slice(0, 64 * 1024));
};

export function resetOrientation(srcBase64, srcOrientation, callback) {
    var img = new Image();

    img.onload = function () {
        var width = img.width,
            height = img.height,
            canvas = document.createElement('canvas'),
            ctx = canvas.getContext("2d");

        // set proper canvas dimensions before transform & export
        if (4 < srcOrientation && srcOrientation < 9) {
            canvas.width = height;
            canvas.height = width;
        } else {
            canvas.width = width;
            canvas.height = height;
        }

        // transform context before drawing image
        switch (srcOrientation) {
            case 2: ctx.transform(-1, 0, 0, 1, width, 0); break;
            case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
            case 4: ctx.transform(1, 0, 0, -1, 0, height); break;
            case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
            case 6: ctx.transform(0, 1, -1, 0, height, 0); break;
            case 7: ctx.transform(0, -1, -1, 0, height, width); break;
            case 8: ctx.transform(0, -1, 1, 0, 0, width); break;
            default: break;
        }

        // draw image
        ctx.drawImage(img, 0, 0);

        // export base64
        callback(canvas.toDataURL());
    };

    img.src = srcBase64;
}

One liner anyone?

I haven't seen anyone mention the browser-image-compression library. It's got a helper function perfect for this.

Usage: const orientation = await imageCompression.getExifOrientation(file)

Such a useful tool in many other ways too.

I created a class wrapped in an ES6 module that solves exactly this.

It's 103 lines, no dependencies, and fairly nicely structured and documented, meant to be easy to modify/reuse.

Handles all 8 possible orientations, and is Promise-based.

Here you go, hope this still helps someone: https://gist.github.com/vdavid/3f9b66b60f52204317a4cc0e77097913

Wunderbart's post worked for me combined with statler's improvements. Adding a few more comments and syntax cleanup, and also passing back the orientation value and I have the following code feel free to use. Just call readImageFile() function below and you get back the transformed image and the original orientation.

const JpegOrientation = [
    "NOT_JPEG",
    "NORMAL",
    "FLIP-HORIZ",
    "ROT180",
    "FLIP-HORIZ-ROT180",
    "FLIP-HORIZ-ROT270",
    "ROT270",
    "FLIP-HORIZ-ROT90",
    "ROT90"
];


//Provided a image file, determines the orientation of the file based on the EXIF information.
//Calls the "callback" function with an index into the JpegOrientation array. 
//If the image is not a JPEG, returns 0. If  the orientation value cannot be read (corrupted file?) return -1.
function getOrientation(file, callback) {
    
    const reader = new FileReader();
    reader.onload = (e) => {

        const view = new DataView(e.target.result);
        
        if (view.getUint16(0, false) !== 0xFFD8) {
            return callback(0);  //NOT A JPEG FILE
        }
        
        const length = view.byteLength;
        let offset = 2;
        while (offset < length) {
            
            if (view.getUint16(offset+2, false) <= 8)   //unknown?
                return callback(-1);
            
            const marker = view.getUint16(offset, false);
            offset += 2;
            if (marker === 0xFFE1) {
                
                if (view.getUint32(offset += 2, false) !== 0x45786966) 
                    return callback(-1); //unknown?
                

                const little = view.getUint16(offset += 6, false) === 0x4949;
                offset += view.getUint32(offset + 4, little);
                const 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));   //found orientation code
                    }
                }
            }
            else if ((marker & 0xFF00) !== 0xFF00) {
                break;
            }
            else { 
                offset += view.getUint16(offset, false);
            }
        }
        
        return callback(-1); //unknown?
    };
    reader.readAsArrayBuffer(file);
}

//Takes a jpeg image file as base64 and transforms it back to original, providing the
//transformed image in callback.  If the image is not a jpeg or is already in normal orientation,
//just calls the callback directly with the source.
//Set type to the desired output type if transformed, default is image/jpeg for speed.
function resetOrientation(srcBase64, srcOrientation, callback, type = "image/jpeg") {
    
    if (srcOrientation <= 1) {  //no transform needed
        callback(srcBase64);
        return;
    }
    
    const img = new Image();    

    img.onload = () => {
        const width = img.width;
        const height = img.height;
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext("2d");

        // set proper canvas dimensions before transform & export
        if (4 < srcOrientation && srcOrientation < 9) {
            canvas.width = height;
            canvas.height = width;
        } else {
            canvas.width = width;
            canvas.height = height;
        }

        // transform context before drawing image
        switch (srcOrientation) {
              
              //case 1: normal, no transform needed
              
              case 2:  
                  ctx.transform(-1, 0, 0, 1, width, 0); 
                  break;
              case 3:
                  ctx.transform(-1, 0, 0, -1, width, height); 
                  break;
              case 4: 
                  ctx.transform(1, 0, 0, -1, 0, height); 
                  break;
              case 5: 
                  ctx.transform(0, 1, 1, 0, 0, 0); 
                  break;
              case 6: 
                  ctx.transform(0, 1, -1, 0, height, 0); 
                  break;
              case 7: 
                  ctx.transform(0, -1, -1, 0, height, width); 
                  break;
              case 8: 
                  ctx.transform(0, -1, 1, 0, 0, width); 
                  break;
              default: 
                  break;
        }

        // draw image
        ctx.drawImage(img, 0, 0);

        //export base64
        callback(canvas.toDataURL(type), srcOrientation);
    };

    img.src = srcBase64;
};


//Read an image file, providing the returned data to callback. If the image is jpeg
//and is transformed according to EXIF info, transform it first.
//The callback function receives the image data and the orientation value (index into JpegOrientation)
export function readImageFile(file, callback) {

    getOrientation(file, (orientation) => {

        console.log("Read file \"" + file.name + "\" with orientation: " + JpegOrientation[orientation]);

        const reader = new FileReader();
        reader.onload = () => {  //when reading complete

            const img = reader.result;
            resetOrientation(img, orientation, callback);
        };
        reader.readAsDataURL(file);  //start read
        
    });
}

In my case, exif-auto-rotate library is what I needed.

I had base64 format image coming from backend and I had to rotate it to its correct orientation before using it. It returns you base64 after changing the rotation, which is a plus for me too.

Here is the npm link for that library: https://www.npmjs.com/package/exif-auto-rotate

This answer is for Angular people, here is a package that helps you find the orientation.

In the below example I have used changeEvent for a input tag but you can use this if you have a dataUrl

If you don't have the dataUrl , you can convert the File -> dataUrl or blob -> dataUrl easily. I Have attached File -> convertor function that helps me convert my selected image File to dataUrl

https://www.npmjs.com/package/ngx-image-compress This is the package npm i ngx-image-compress

import { DOC_ORIENTATION, NgxImageCompressService } from 'ngx-image-compress';

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss'],
})

export class Test {
     constructor(private imageCompress: NgxImageCompressService) {}

      async onFileSelected(event: any) {
         const file: File = event.target.files[0]
         const dataUrl : string = await this.fileToDataURL(file)
         const orientation : DOC_ORIENTATION = await this.imageCompress.getOrientation(file)

      }
      
     fileToDataURL(blob: Blob): Promise<string> {
        return new Promise<string>((resolve, reject) => {
           const reader = new FileReader();
           reader.onload = _e => resolve(reader.result as string);
           reader.onerror = _e => reject(reader.error);
           reader.onabort = _e => reject(new Error("Read aborted"));
           reader.readAsDataURL(blob);
    });
  }

}
Related