Skip to content
This repository has been archived by the owner on Aug 1, 2020. It is now read-only.

Handling EXIF Image Orientation #120

Closed
bameyrick opened this issue Dec 3, 2014 · 58 comments
Closed

Handling EXIF Image Orientation #120

bameyrick opened this issue Dec 3, 2014 · 58 comments

Comments

@bameyrick
Copy link

Firstly, thanks for the great plugin! However I've noticed a small issue; When loading an image in with EXIF orientation, everything works as expected until the "getDataURL" method is called. The generated image does not crop correctly and the rotation of the cropped image is incorrect.

Below I've provided screen shots of the chosen crop area and the image generated by "Get Data URL (JPG)". The image was taken on an iPad in portrait mode.

img_0112

img_0113

The white area in the generated image only seems to happen when a crop is taken near the bottom the image.

Thanks

@fengyuanchen
Copy link
Owner

Thank you very much! I will handle it if it is possible.

@fengyuanchen
Copy link
Owner

I have fixed a bug when replace the image. Please try the new version 0.7.5 to check if it still occurs this problem.

@spedley
Copy link

spedley commented Dec 8, 2014

My thanks as well for the fabulous plugin - I really appreciate the hard work you've clearly put into it.

I've updated to version 0.7.5 and unfortunately I'm still having the same issue as reported by @bameyrick. On an iOS device, it often processes a section of the image different from what's been selected in the GUI probably due to how iOS adjusts the image display based on EXIF Orientation data.

In my tests, an EXIF Orientation of 6 produces the issue because iOS rotates the image 90 degrees clockwise so that it displays 'correctly'. If that's the case, then I would also guess that EXIF Orientations of 3 and 8 would cause problems too since iOS would rotate those images 180 degrees and 90 degrees counter-clockwise respectively.

Forgive my corny test picture, but it shows the Chrome browser rendering the same photograph (with an EXIF Orientation of 6) on my desktop Mac and my iPhone 5:

cropper_issue

If you have any suggestions on what I might be able to do to address this, they'd be greatly appreciated.

Thanks again!

@lucascurti
Copy link

Having the same issue. Anybody found a workaround for this issue?

@fengyuanchen
Copy link
Owner

Maybe it is more easy to handle the EXIF Orientation on the server-side.

@jasonterando
Copy link

First, thanks for an outstanding plug-in!!! I poked around to see if there was a client-side library I could use to do the EXIF correctio. There are a couple of JS EXIF readers (like https://github.com/jseidelin/exif-js), and I think I can use that info, load the image into a canvas, rotate the image in the canvas, and then get the data. I'll play around with this more and post any luck I have.

In the meantime, for .NET land, I put together a c# static method (which I wired up to a WebAPI, but you can use it with WCF or whatever). You'll need to add a project reference to System.Drawing (and add namespaces to your class for System.Drawing, System.Drawing.Imaging and System.Text).

        /// <summary>
        /// Removes EXIF rotation from an image
        /// </summary>
        /// <param name="URI"></param>
        /// <returns></returns>
        public static string RemoveEXIFRotation(string URI)
        {
            // Decode the URI information
            string base64Data = Regex.Match(URI, @"data:image/(?<type>.+?),(?<data>.+)").Groups["data"].Value;
            byte[] binData = Convert.FromBase64String(base64Data);

            using (var stream = new MemoryStream(binData))
            {
                Bitmap bmp = new Bitmap(stream);

                if(Array.IndexOf(bmp.PropertyIdList, 274) > -1)
                {
                    var orientation = (int) bmp.GetPropertyItem(274).Value[0];
                    switch(orientation)
                    {
                        case 1:
                            // No rotation required.
                            return URI;
                        case 2:
                            bmp.RotateFlip(RotateFlipType.RotateNoneFlipX);
                            break;
                        case 3:
                            bmp.RotateFlip(RotateFlipType.Rotate180FlipNone);
                            break;
                        case 4:
                            bmp.RotateFlip(RotateFlipType.Rotate180FlipX);
                            break;
                        case 5:
                            bmp.RotateFlip(RotateFlipType.Rotate90FlipX);
                            break;
                        case 6:
                            bmp.RotateFlip(RotateFlipType.Rotate90FlipNone);
                            break;
                        case 7:
                            bmp.RotateFlip(RotateFlipType.Rotate270FlipX);
                            break;
                        case 8:
                            bmp.RotateFlip(RotateFlipType.Rotate270FlipNone);
                            break;
                    }
                    // This EXIF data is now invalid and should be removed.
                    bmp.RemovePropertyItem(274);
                }

                ImageCodecInfo jgpEncoder = ImageCodecInfo.GetImageDecoders().First(x => x.FormatID == ImageFormat.Jpeg.Guid);

                // Create an Encoder object based on the GUID
                // for the Quality parameter category.
                System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality;

                // Create an EncoderParameters object.
                // An EncoderParameters object has an array of EncoderParameter
                // objects. In this case, there is only one
                // EncoderParameter object in the array.
                EncoderParameters myEncoderParameters = new EncoderParameters(1);

                EncoderParameter myEncoderParameter = new EncoderParameter(myEncoder, 100L);
                myEncoderParameters.Param[0] = myEncoderParameter;

                using(MemoryStream output = new MemoryStream(binData.Length))
                {
                    bmp.Save(output, jgpEncoder, myEncoderParameters);
                    output.Position = 0;
                    int len = (int) output.Length;
                    byte[] results = new byte[len];
                    output.Read(results, 0, len);

                    return string.Format("data:image/jpg;base64,{0}", Convert.ToBase64String(results));
                }
            }
        }

Edit: Make sure to call this before loading image into cropper, not after

@fengyuanchen
Copy link
Owner

Thank you @jasonterando .

@jacobsvante
Copy link

Did you guys look further into using exif-js?

@bbrooks
Copy link

bbrooks commented Jul 10, 2015

If you're getting an image from the user, you can use https://github.com/blueimp/JavaScript-Load-Image to get it into the correct orientation before using cropper:

$('#yourFileInput').on('change', function(e) {
    e.preventDefault();
    e = e.originalEvent;
    var target = e.dataTransfer || e.target,
        file = target && target.files && target.files[0],
        options = {
            canvas: true
        };

    if (!file) {
        return;
    }

    // Use the "JavaScript Load Image" functionality to parse the file data
    loadImage.parseMetaData(file, function(data) {

        // Get the correct orientation setting from the EXIF Data
        if (data.exif) {
            options.orientation = data.exif.get('Orientation');
        }

        // Load the image from disk and inject it into the DOM with the correct orientation
        loadImage(
            file,
            function(canvas) {
                var imgDataURL = canvas.toDataURL();
                var $img = $('<img>').attr('src', imgDataURL);
                $('body').append($img);

                // Initiate cropper once the orientation-adjusted image is in the DOM
                $img.cropper();
            },
            options
        );
    });
});

@kingwrcy
Copy link

@bbrooks nice work,you saved my day,awesome plugin.

@dekortage
Copy link

I encountered the same problem with iOS devices while I was adapting the Crop Avatar example to my own purposes. The preview would look fine in the web browser, but once uploaded and cropped (and manipulated on the back-end), it had the wrong orientation.

I solved this on the PHP side. In that example's cropper.php file, in the setFile function, starting at line 54, there is this block:

if ($result) {
    $this -> src = $src;
    $this -> type = $type;
    $this -> extension = $extension;
    $this -> setDst();
} else {
    $this -> msg = 'Failed to save file';
}

Into this block, I incorporated the solution described on StackExchange by user462990. So that whole block now reads:

if ($result) {

    $exif = exif_read_data($src);
    if (!empty($exif['Orientation'])) {
        $imageToRotate = imagecreatefromjpeg($src);
        switch ($exif['Orientation']) {

            case 3:
                $imageToRotate = imagerotate($imageToRotate, 180, 0);
                break;

            case 6:
                $imageToRotate = imagerotate($imageToRotate, -90, 0);
                break;

            case 8:
                $imageToRotate = imagerotate($imageToRotate, 90, 0);
                break;

        }
        imagejpeg($imageToRotate, $src, 90);
        imagedestroy($imageToRotate);
    }

    $this -> src = $src;
    $this -> type = $type;
    $this -> extension = $extension;
    $this -> setDst();

} else {
     $this -> msg = 'Failed to save file';
}

There's probably a better way to handle this, but this works for me for now.

@fengyuanchen
Copy link
Owner

@dekortage If the user rotate the image with cropper on browser side, and you rotate it again on server side, then what will happen?

@edbond
Copy link

edbond commented Aug 29, 2015

I am using exif-js to determine orientation and rotate initially in browser. Server uses data from cropper. Here is my code. Note that there any 1-8 orientations in exif.

      EXIF.getData img.get(0), () ->
        orientation = EXIF.getTag(this, "Orientation")
        # console.log "orientation", orientation

        img.cropper
          strict: false
          aspectRatio: 1.47
          crop: (e) ->
            $("#photo_offset").val(JSON.stringify(e))

        # rotate according to orientation
        switch orientation
          when 2
            img.cropper('scale', -1, 1) # Flip horizontal
          when 3
            img.cropper('scale', -1)
          when 4
            img.cropper('scale', 1, -1)
          when 5
            img.cropper('scale', -1, 1)
            img.cropper('rotate', -90)
          when 6
            img.cropper('rotate', 90)
          when 7
            img.cropper('scale', 1, -1)
            img.cropper('rotate', -90)
          when 8
            img.cropper('rotate', -90)

        $(".rotate-left").off("click")
        $(".rotate-left").on "click", (e) ->
          e.preventDefault()
          e.stopPropagation()
          img.cropper('rotate', 90)

On server I'm using ruby-on-rails and carrierwave to handle uploads. Code uses Rmagick. Here is code. It extracts x,y,w,h,rotate,scale values that cropper sends to server. strip! removes exif info.

    manipulate! do |img|
      Rails.logger.debug "[PHOTO] offset: #{model.photo_offset.pretty_inspect}".yellow
      x, y, w, h, rotate, scaleX, scaleY = model.photo_offset.values_at("x", "y",
        "width", "height", "rotate", "scaleX", "scaleY").map { |v| v.to_f.round(2) }

      crop = "#{w}x#{h}!+#{x}+#{y}"
      Rails.logger.debug "[PHOTO] crop: #{crop.inspect}".green

      # img.auto_orient!
      img.flip! if scaleY == -1
      img.flop! if scaleX == -1
      img.rotate!(rotate) if rotate.present? && !rotate.zero?
      img.crop!(x,y,w,h)
      img.strip!

      img = yield(img) if block_given?
      img
    end

Blog post about exif rotations: http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
and there is link to example images which may be used to test:
https://github.com/recurser/exif-orientation-examples

I don't have any problems with this example images, however MacOs guys complains so the code is not 100% complete.

HTH

@dekortage
Copy link

@fengyuanchen -- it seems to work fine in my limited testing. The client side previews fine, and the transformations are sent up to the server as if the image is oriented correctly. It's just that the iOS device uploads the image at a 90° angle before applying the cropping/transformations. The server needs to reorient the file back to what the user saw client-side, before applying the server-side transformations.

However, I would definitely welcome more extensive inquiry/experimentation into this. I only have a couple of iOS devices at my disposal.

@MarcusJT
Copy link

MarcusJT commented Oct 7, 2015

If the root problem is that sometimes the image rendered to a canvas is correctly orientated and sometimes it isn't, then given an image with width X and height Y then can't we determine which is longest and store that as H and the shortest as W, then render the image to a canvas of size HxH, then check the pixels at (W,H) and (H,W) to see which way round the image has been rendered, then rotate and crop the area as required, giving us the correctly orientated image in all cases, from which point we can then do anything we want, with no server manipulation needed?

@fengyuanchen
Copy link
Owner

@MarcusJT What about a square image?

@MarcusJT
Copy link

MarcusJT commented Oct 7, 2015

Ha! Good point... damn...

@rern
Copy link

rern commented Nov 17, 2015

  1. Get exif orientation with exif-js
  2. new FileReader for new Image
  3. Create canvas
  4. context.transform and context.drawImage
  5. Get rotated image from canvas

Update - 2015-11-18 :
Get exif orientation with this handy function getOrientation() instead of exif-js.

Edit - 2015-11-19 :
'getOrientation' - return '0' on undefined.
'ori === 1' - must 'drawImage' to canvas as well.
'canvas' - must be set width and height.
'image' - set height to keep size within page.
Full html page.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/cropper.css">
</head>
<body>

<input type="file" id="file" accept=".jpg, .jpeg, .png, .gif">

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="js/plugin/cropper.min.js"></script>

<script>
$(document).ready(function () {

$('#file').change(function () {
    var file = this.files[0];

    getOrientation(file, function (ori) { // 1. get exif
        reader(file, ori ? ori : 1);
    });
});

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;
        var offset = 2;
        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) {
                var little = view.getUint16(offset += 8, 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(0); // +- edit
    };
    reader.readAsArrayBuffer(file.slice(0, 64 * 1024));
}

function reader(file, ori) {
    var reader = new FileReader(); // 2. FileReader

    reader.onload = function (e) {
        var img = new Image();
        img.onload = function () {
            if (ori === 1 || ori === 3) {
                var w = this.width;
                var h = this.height;
            } else {
                var h = this.width;
                var w = this.height;
            }

            //if (ori === 1) { // -- removed
            //    var img0 = img; // -- removed
            //} else { // -- removed

            var canvas = document.createElement('canvas'); // 3. canvas
            var ctx = canvas.getContext('2d');
            canvas.width = w; // ++ added
            canvas.height = h; // ++ added

            if (ori === 1) { // ++ added
                ctx.drawImage(img, 0, 0, w, h); // ++ added
            } else if (ori === 3) { // 4. transform, drawImage
                ctx.transform(-1, 0, 0, -1, w, h);
                ctx.drawImage(img, 0, 0, w, h);
            } else if (ori === 6) {
                ctx.transform(0, 1, -1, 0, w, 0);
                ctx.drawImage(img, 0, 0, h, w);
            } else {
                ctx.transform(0, -1, 1, 0, 0, h);
                ctx.drawImage(img, 0, 0, h, w);
            }

            var img0 = canvas.toDataURL(); // 5. get image
            //} // -- removed
            // +- add max-height to keep size within page
            $('body').html('<img id="image" style="max-height: '+ $(window).height() +'px;">');
            $('#image').prop('src', img0).one('load', function () {
                $(this).cropper();
            });
        }
        img.src = e.target.result;
    }
    reader.readAsDataURL(file);
}

});
</script>

</body>
</html>

@50bbx
Copy link

50bbx commented Nov 18, 2015

@rern really great thanks! I used your solution about an hour ago. What I discovered is that in iOS (Chrome and Safari) the orientation is correct, without any workaround but if you try on Chrome desktop with a photo taken with an iPhone (with exif) it will generate the issue. This is is a per-browser issue. I will add more details as soon as I can.

@fengyuanchen
Copy link
Owner

You can try the Loader to translate Exif Orientation by canvas and get a pure image for Cropper.

@rern
Copy link

rern commented Nov 18, 2015

@50bbx - I've tried Chrome on Windows 10. iPhone photos display correctly.
(IE < 10 not support dataView, the solution will not do the trick there.)
BTW I've updated the code, switch out exif-js for a handy function from stackoverflow. May be It's small enough for @fengyuanchen to consider including it in his wonderful code.

@50bbx
Copy link

50bbx commented Nov 18, 2015

@rern Sorry, you are right, I was using half of your solution and half of edbond. I see the image correctly rotated now. The problem is I only see a very small piece. This is the result:
cropper with exif
as you can see it is just the upper left corner of the image. I think the base64 conversion from the canvas doesn't work correctly. I also inspected the element with chrome and the image is not hidden fullsize in the cropper: it appears the image has been cropped by the rotation process in the reader function. I didn't change anything, except where I append the image.

EDIT: I also think there is an error whe orientation is 1. Infact var img0 = img; means that img0 is an HTMLImageElement but then $('#image').prop('src', img0) means that img0 should be a string which is not. The result is that when you append the image to the DOM you get a strange <img id="image" src="[object HTMLImageElement]">.

Please, do not get me wrong, I don't want to say you were wrong, I'm just trying to make this code work. I thank you for your help, it has been really appreciated. :)

@50bbx
Copy link

50bbx commented Nov 18, 2015

@fengyuanchen I've tried, I've used precisely @bbrooks solution but it was so slow I had to revert my changes. I'm sorry I cannot provide that code anymore. But I simply copied and pasted that solution in my code (that is pretty straight forward and doesn't do anything complex nor strange)

@yelhouti
Copy link

@fengyuanchen I think @mjvestal is right, may be you should remove the 2 lines he mentioned from the code (if statement after //modern browsers). by the way thank you @mjvestal.

@yelhouti
Copy link

@Win10Developer I shouldn't be answering here since it's not the best place to ask, but here it is:

$('#imgid').cropper({
    //your other options
    crop: function(e) {
         $('"textid").val($('#imgid').cropper('getCroppedCanvas').toDataURL("image/jpeg"));
   }                            
});

@fsmeier
Copy link

fsmeier commented Feb 17, 2016

@fengyuanchen

with "checkOrientation: false" i have the same problem like @every2ndcountsxc
the image i rotated but now it's quiet compressed.

with "checkOrientation: true" and version 2.2.5 the image is still not in the right direction.. :/

+1 for fixing this...

PS.: really nice plugin 👍

@fsmeier
Copy link

fsmeier commented Feb 17, 2016

@fengyuanchen

i built the latest version like you described, the image is now rotated like it should, but my wrapper has now at the top and at the button space (see image)
The space would be because the browser did not rotate the image in the img-tag (but took its width+height from there)... any ideas?

12722538_10208887672926590_1336080580_o

EDIT: checkOrientation: false/true does the same now

@fengyuanchen
Copy link
Owner

Hi, everyone, please provide your iOS and Safari version here once you need to comment here.

@fsmeier
Copy link

fsmeier commented Feb 18, 2016

iOS 9.2.1 on iPhone 5S

@zilions
Copy link

zilions commented Mar 1, 2016

I'm using iOS 9.2 on an iPhone 6S. All images that I add to the cropper via selecting the "Take Photo" option are rotated incorrectly. Although, if the image is added via an already existing image, using the "Photo Library" option, the image is the correct rotation.

Here is are screenshots of me using the "Take Photo" option:

img_2164
img_2165

@samrayner
Copy link

Can confirm that the following fixed iPhone uploads rotating for me:

rotatable: true
checkOrientation: true

And commenting out
https://github.com/fengyuanchen/cropper/blob/master/src/js/utilities.js#L53-L55

@zilions
Copy link

zilions commented Mar 21, 2016

Can also confirm that the fix from @samrayner works well on Safari. Thanks for sharing.

@zilions
Copy link

zilions commented Apr 8, 2016

@samrayner's solution only seems to work some of the time. I believe it does not work for images that were taken as landscape. Unfortunately, there are still many issues with the cropper and image orientation.

@zilions
Copy link

zilions commented Apr 21, 2016

After weeks of debugging, I have found that @bbrooks solution with the Javascript-Load-Image plugin is the best solution. It may be slightly slower (since it is converting to base64 along with reading meta data), yet it is basically bulletproof. Have tested it in many versions of iOS, Chrome and Firefox, and all the images that used to be rotated incorrectly, wrong aspect ratio and squished now work perfectly as expected. Thanks @bbrooks!

@ghybs
Copy link

ghybs commented May 29, 2016

Hi,

I am also facing this problem.
I experienced the exact same symptoms as many other issues on the repo (90deg left-rotated image, inverted height-width aspect ratio / distortion, incorrect cropped area only once getting data URL, depending on options, external workaround tried, etc.)

In my case I develop an hybrid (Cordova-based) iOS app, so I use this plugin within a UIWebView (not Safari).

Therefore the IS_SAFARI browser sniffing test was not catching up the WebView behaving like on Safari.
Adding an extra platform sniffing test (to detect iPhone / iPad / iPod) and OR'ing it with IS_SAFARI solved the issue for me.

@yelhouti
Copy link

yelhouti commented May 29, 2016

Hi @ghybs can you say how you did the test for the UIWebView? I have the same problem with cordova

@kaynz
Copy link

kaynz commented May 29, 2016

@ghybs Could you create a pull request?

@ghybs
Copy link

ghybs commented May 29, 2016

Hi,

As for detecting the UIWebView, for now I just detect an iOS device:
http://stackoverflow.com/questions/4460205/detect-ipad-iphone-webview-via-javascript

This is not bullet proof in the case of a Web App, as I guess people could still use another browser on an iOS device.
Hopefully this already covers most cases.

I will try to make a PR when I have time.

@1fabiopereira
Copy link

I was having this problem, I did what @samrayner suggested and worked perfectly.

@GoToBoy
Copy link

GoToBoy commented Jan 16, 2017

fixed this using @bbrooks thanks

@faradoxuz
Copy link

var img0 = canvas.toDataURL(); // 5. get image
@rern add mime type like
canvas.toDataURL('image/jpeg');
in ortder not to have big size

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests