fengyuanchen / cropper

⚠️ [Deprecated] No longer maintained, please use https://github.com/fengyuanchen/jquery-cropper
MIT License
7.76k stars 1.74k forks source link

Handling EXIF Image Orientation #120

Closed bameyrick closed 7 years ago

bameyrick commented 9 years ago

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 commented 9 years ago

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

fengyuanchen commented 9 years ago

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.

superniftydev commented 9 years ago

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 commented 9 years ago

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

fengyuanchen commented 9 years ago

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

jasonterando commented 9 years ago

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 commented 9 years ago

Thank you @jasonterando .

jacobsvante commented 9 years ago

Did you guys look further into using exif-js?

bbrooks commented 9 years ago

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 commented 9 years ago

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

dekortage commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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 commented 9 years ago

@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 commented 8 years ago

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 commented 8 years ago

@MarcusJT What about a square image?

MarcusJT commented 8 years ago

Ha! Good point... damn...

rern commented 8 years ago
  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 commented 8 years ago

@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 commented 8 years ago

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

rern commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

@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)

rern commented 8 years ago

@50bbx - You're right. My bad. There're some flaw in the code. It's been corrected. Though in IE, I don't know why it takes ages to load a standard iOS photo. (30s+ vs 3s on Chrome)

fengyuanchen commented 8 years ago

As of v2.2.0, the Cropper will read the Exif Orientation information and override it to its default value: 1. This may fix this problem.

kaynz commented 8 years ago

Not fixed in 2.2.4 =(

fengyuanchen commented 8 years ago

@kaynz If your browser supports Typed Array (iOS Safari 8.4+), then this issue may be fixed since Cropper v2.2.0.

Reference: http://caniuse.com/TypedArrays

kaynz commented 8 years ago

@fengyuanchen thanks for the quick answer. I am using iOS Safari 9, which supports Typed Array.

fengyuanchen commented 8 years ago

@kaynz Then I don't know why. Sorry!

kaynz commented 8 years ago

@fengyuanchen I just set checkOrientation to false and now it is working on iOS Safari. Any ideas?

fengyuanchen commented 8 years ago

@kaynz Are you using the latest version (v2.2.4)?

kaynz commented 8 years ago

@fengyuanchen yes.

fengyuanchen commented 8 years ago

@kaynz Do you catch any error in the console?.

mjvestal commented 8 years ago

I also set checkOrientation to false, and it fixes the issue on iOS Safari, but broke on desktop browsers. See my comment here: https://github.com/fengyuanchen/cropper/issues/509#issuecomment-172633656

every2ndcountsxc commented 8 years ago

@mjvestal I followed your suggested changes by commenting out those lines, and I also set checkOrientation to false in my options, but I'm still having issues. Basically, when I choose to take a photo from my camera, on FIRST file load the width is distorted. When I then choose to take different photo, there the new photo that loads isn't distorted.

fengyuanchen commented 8 years ago

@every2ndcountsxc You can build the dist files from master branch on your computer and try it again:

git clone https://github.com/fengyuanchen/cropper.git
cd cropper
npm install
gulp release
every2ndcountsxc commented 8 years ago

Thanks @fengyuanchen but I don't believe "trying again" is a helpful solution.

I do, however, think my issue is something separate from the main issue(s) in this thread.

Using your own demo on my iPhone 6 (iOS9), I imported a file by taking a photo with the camera. This is the result:

img_1823

As you can see, the image is distorted, as if the file reader was expecting a landscape image.

When I originally thought my problem was just a matter of me not implementing the plugin correctly, I'm now realizing that there is something going on with the way the mobile device is interpreting the image from the camera.

Thoughts on how to solve this? Thanks!

mjvestal commented 8 years ago

@every2ndcountsxc, the thing that works for me is NOT setting checkOrientation to false + commenting out those lines

every2ndcountsxc commented 8 years ago

@mjvestal thank you. That was the kind of solution I was looking for. Works like a charm. Cheers!

DotNetFiver commented 8 years ago

In the first image on this post, there is a button 'Get data URL' which I assume puts the dataurl string into the text box also shown in the image. I'm looking for sample code to get this as I haven't been able to acquire the canvas dataurl string in javascript (my javascript knowledge is lacking a a little, I'm a dot net developer) Can anybody provide the snippet of javascript that creates dataurl?

yelhouti commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

@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 :+1:

fsmeier commented 8 years ago

@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 commented 8 years ago

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

fsmeier commented 8 years ago

iOS 9.2.1 on iPhone 5S

zilions commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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

zilions commented 8 years ago

@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.