Closed bameyrick closed 7 years ago
Thank you very much! I will handle it if it is possible.
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.
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:
If you have any suggestions on what I might be able to do to address this, they'd be greatly appreciated.
Thanks again!
Having the same issue. Anybody found a workaround for this issue?
Maybe it is more easy to handle the EXIF Orientation on the server-side.
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
Thank you @jasonterando .
Did you guys look further into using exif-js?
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
);
});
});
@bbrooks nice work,you saved my day,awesome plugin.
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.
@dekortage If the user rotate the image with cropper on browser side, and you rotate it again on server side, then what will happen?
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
@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.
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?
@MarcusJT What about a square image?
Ha! Good point... damn...
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>
@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.
You can try the Loader to translate Exif Orientation by canvas and get a pure image for Cropper.
@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.
@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: 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. :)
@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)
@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)
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.
Not fixed in 2.2.4 =(
@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
@fengyuanchen thanks for the quick answer. I am using iOS Safari 9, which supports Typed Array.
@kaynz Then I don't know why. Sorry!
@fengyuanchen I just set checkOrientation to false and now it is working on iOS Safari. Any ideas?
@kaynz Are you using the latest version (v2.2.4)?
@fengyuanchen yes.
@kaynz Do you catch any error in the console?.
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
@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.
@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
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:
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!
@every2ndcountsxc, the thing that works for me is NOT setting checkOrientation to false + commenting out those lines
@mjvestal thank you. That was the kind of solution I was looking for. Works like a charm. Cheers!
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?
@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.
@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"));
}
});
@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:
@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?
EDIT: checkOrientation: false/true does the same now
Hi, everyone, please provide your iOS and Safari version here once you need to comment here.
iOS 9.2.1 on iPhone 5S
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:
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
Can also confirm that the fix from @samrayner works well on Safari. Thanks for sharing.
@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.
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.
The white area in the generated image only seems to happen when a crop is taken near the bottom the image.
Thanks