xamarin / Essentials

Xamarin.Essentials is no longer supported. Migrate your apps to .NET MAUI, which includes Maui.Essentials.
https://aka.ms/xamarin-upgrade
Other
1.52k stars 505 forks source link

[Bug] MediaPicker CapturePhotoAsync - Orientation of photo rotated 90 degrees #1514

Closed mossywossy closed 2 years ago

mossywossy commented 4 years ago

Description

After calling CapturePhotoAsync and setting the ImageSource to an Image, the resulting image is flipped 90 degrees (sideways) rather than retaining the orientation that the photo was taken in.

Steps to Reproduce

  1. Call CapturePhotoAsync with device in portrait.
  2. Set resulting image to image source of Image element
  3. Image is sideways

Expected Behavior

I would expect that the photo retains the orientation the device was in when the photo was taken.

Actual Behavior

The photo is 90 degrees rotated

Basic Information

Screenshots

Reproduction Link

AndrewBohdanovich commented 3 years ago

any updates?

groege commented 3 years ago

As described in #1640: RotateImage flag should be implemented. As far as I understand the problem ist, that the image is stored ignoring the phones rotation and only the exif data in the picture contains the necessary rotation... Therefore we need the option RotateImage which should rotate the image according to the exif data and remove the rotation data from exif. Also it should be possible to retain Exif data as already requested in multiple duplicate issues (see #1640 for an overview)

ColdstarJoey commented 3 years ago

Any update on this at all ?

mmunchandersen commented 3 years ago

Did anyone develop a media picker that handles the rotation bug?

nicoriff commented 3 years ago

Is this issue being treated?. I implemented this library and resulted to be worst than what I was using before.

AndrewBohdanovich commented 3 years ago

i'm not sure about actuality of this information https://stackoverflow.com/questions/10850184/ios-image-get-rotated-90-degree-after-saved-as-png-representation-data but if we save photo as PNG on iOS with using UIImagePickerController we reproduce this bug

nicoriff commented 3 years ago

No time to lose. I ended up going to Xam.Plugin from @jamesmontemagno. That library is still working fine.

mmunchandersen commented 3 years ago

@nicoriff does it work on iOS14?

nicoriff commented 3 years ago

@mmunchandersen Yes in iOS 14.4 works great.

teymur371 commented 3 years ago

It seems I'm not the only one moving back to Xam.Plugin

javanjoel commented 3 years ago

@teymur371 ... no you're not the only one. For iOS I just implemented a custom dependency to take pictures and video.

I'm surprised the Xamarin team doesn't prioritize something like this.

nicoriff commented 3 years ago

@javanjoel I have the same impression as you have. It hs been an enigma for me why the xamarin team hasn't invested more resources on make a good camera control.

rholdsworth commented 3 years ago

We are also going back to the plugin due to this. It appears that PickPhotoAsync has the same issue on Android so hopefully this gets addressed at the same time.

javanjoel commented 3 years ago

Hard to believe that a "mature" framework such as Xamarin can't even take photos properly!

Hackmodford commented 3 years ago

Just had this bug bite me. What do we need to do to fix this?

bmacombe commented 3 years ago

@Hackmodford

You could try this workaround using FFImageLoading.

https://github.com/xamarin/Essentials/issues/1640#issuecomment-811915611

javanjoel commented 3 years ago

@bmacombe the problem with that solution is that we have to make an assumption that the image always needs to be rotated 90 deg. We cannot guarantee that will always be the case.

bmacombe commented 3 years ago

@javanjoel Yeah it's not perfect, could probably use the Device Display Information tools in Essentials to attempt to figure that out.

https://docs.microsoft.com/en-us/xamarin/essentials/device-display?context=xamarin%2Fxamarin-forms&tabs=android

Again, just a workaround until it's correctly fixed at the platform level.

Hackmodford commented 3 years ago

@bmacombe Tried that and it causes the opposite problem if the device is rotated.

bmacombe commented 3 years ago

Yes, you'll have to detect if the device is in portrait mode or not and they only rotate if needed

bmacombe commented 3 years ago

Here is a lib that might help with reading the exif data

https://github.com/drewnoakes/metadata-extractor-dotnet

        var meta = ImageMetadataReader.ReadMetadata(await photo.OpenReadAsync());
        var subIfd0Directory = meta.OfType<ExifIfd0Directory>().FirstOrDefault();
        var orientation = subIfd0Directory?.GetDescription(ExifDirectoryBase.TagOrientation);

The code above will get you the orientation of the photo, then you can decide if you need to rotate

Update: might be different tags on iOS, my initial test was UWP, but at least an option to try to get them Update 2: seems the resulting stream coming back from MediaPicker.CapturePhotoAsync() on iOS doesn't have the orientation tag at all. So this method likely won't work. Update 3: using MediaPicker.PickPhotoAsync(), the stream returned does have the orientation.

stoff99 commented 3 years ago

Is there any update to add this feature to get the image already in correct orientation?

lndalmd commented 3 years ago

An effective solution to such an old bug would be greatly appreciated.

ChristophWeigert commented 3 years ago

This is also a blocking point for us. Will there be any 1.7 Version? Or do we have to wait for MAUI?

glintpursuit commented 3 years ago

Not good.

I have removed everything from Xam plugin, I have to go back to same plugin. :-(

glintpursuit commented 3 years ago

Seems like it is resolved but not merged.

https://github.com/xamarin/Essentials/pull/1639

glintpursuit commented 3 years ago

@teymur371 ... no you're not the only one. For iOS I just implemented a custom dependency to take pictures and video.

I'm surprised the Xamarin team doesn't prioritize something like this.

@teymur371 Can you give more details on how you have implemented custom dependancy ?

glintpursuit commented 3 years ago

I have implemented custom dependency, you will start getting metadata of image

public class MediaPicker : IMediaPicker
    {      
        public async Task<FileResult> CapturePhotoAsync(MediaPickerOptions options)
        {
            TaskCompletionSource<FileResult> taskCompletionSource = new TaskCompletionSource<FileResult>();

            if (await Permissions.RequestAsync<Permissions.Camera>() == PermissionStatus.Granted)
            {
                //Create an image picker object
                UIImagePickerController imagePicker = new()
                {
                    SourceType = UIImagePickerControllerSourceType.Camera,
                    MediaTypes = new string[] { UTType.Image },

                };

                if (string.IsNullOrEmpty(options.Title))
                {
                    imagePicker.Title = options.Title;
                }

                var vc = Platform.GetCurrentUIViewController();

                imagePicker.AllowsEditing = false;
                await vc.PresentViewControllerAsync(imagePicker, true);
                imagePicker.FinishedPickingMedia += async(object sender, UIImagePickerMediaPickedEventArgs e) =>
                {
                    UIImage uimage = e.Info[UIImagePickerController.OriginalImage] as UIImage;

                    var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
                    string jpgFilename = System.IO.Path.Combine(documentsDirectory, System.Guid.NewGuid() + ".jpg");
                    NSData imgData = uimage.AsJPEG();

                    NSError err = null;

                    await vc.DismissViewControllerAsync(true);

                    if (imgData.Save(jpgFilename, false, out err))
                    {

                        taskCompletionSource.TrySetResult(new FileResult(jpgFilename));
                    }
                    else
                    {
                        taskCompletionSource.TrySetException(new Exception("Unable to save the image" + err?.ToString()));
                    }

                    imagePicker?.Dispose();
                    imagePicker = null;
                };

                imagePicker.Canceled += async(object sender, EventArgs e) =>
                {
                    await vc.DismissViewControllerAsync(true);
                    taskCompletionSource.TrySetResult(null);
                    imagePicker?.Dispose();
                    imagePicker = null;
                };
            }
            else
            {
                taskCompletionSource.TrySetException(new PermissionException("Camera permission not granted"));
            }

            return await taskCompletionSource.Task;
        }

        class CameraDelegate : UIImagePickerControllerDelegate
        {
            public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info)
            {
                picker.DismissModalViewController(true);                
            }
        }       
    }
Harshadcse commented 3 years ago

@glintpursuit : Your solution is worked perfectly .

flimtix commented 3 years ago

Are there any fixes now? Because it still doesn't work... 😕

kevinphihoang commented 3 years ago

If anyone needs another workaround, https://stackoverflow.com/questions/54672643/xamarin-forms-camera-picture-is-left-rotated-when-comes-to-ui-in-ios This thread shows how to use MediaPlugin to avoid the issue.

JamesMerlin commented 3 years ago

Any updates on this? The app I'm developing is business critical and need this function to work as intended. Not only is the portrait an issue, but also affects landscape. The orientation or the top anchor point of the image is to the right of the camera view if it's done in portrait. So it's not a matter of it being rotated, it's not taking into account which way round the camera is orientated so it will always have one side facing up.

scriptBoris commented 3 years ago

Any update?

flimtix commented 3 years ago

@jmarti326 file = await MediaPicker.CapturePhotoAsync(); ImageSource.FromFile(file.FullPath); Solves the problem. 😂

You should not do that! If you look closely at the documentation you can see at the botton an important message. Tip: The FullPath property does not always return the physical path to the file. To get the file, use the OpenReadAsync method.

scriptBoris commented 3 years ago

Xamarin team, any updates? News? Hello?

dimonovdd commented 3 years ago

am I the only one who does not consider this a bug?

stoff99 commented 3 years ago

@dimonovdd what do you mean? Using an extension like this to get a not rly usable result, its a bug. If you would have the correct infos in the exif data of the photo to rotate it back, it would be also ok. But in this case you have no chance because the exif data is lost.

angelru commented 3 years ago

In android the image is rotated too, any solution?

Hackmodford commented 3 years ago

This is how I fixed it in iOS. Essentially I just recreated the code with some modifications. I don't remember what the exact modification was...

https://gist.github.com/Hackmodford/9060579ccab4fd0917061ced474cdb05

angelru commented 3 years ago

and on android?

Hackmodford commented 3 years ago

I didn't have the problem on Android. You could probably use a similar approach though.

angelru commented 3 years ago

the problem is that I do a resize and it turns on android

Hackmodford commented 3 years ago

Is the resize operation a part of this library?

angelru commented 3 years ago

I have this combined with MediaPicker and it works fine, I have not done the iOS one yet. It sure can be improved.

 public class PhotoPickerService : IPhotoPickerService
    {
        public async Task<byte[]> ImageToArrayAsync(string path)
        {
            try
            {
                var exif = new Android.Media.ExifInterface(path);
                string orientation = exif.GetAttribute(Android.Media.ExifInterface.TagOrientation);

                //Get the bitmap.
                var originalImage = BitmapFactory.DecodeFile(path);

                //Set imageSize and imageCompression parameters.
                var imageSize = .40;
                var imageCompression = 45;

                //Resize it and then compress it to Jpeg.
                var width = originalImage.Width * imageSize;
                var height = originalImage.Height * imageSize;
                var scaledImage = Bitmap.CreateScaledBitmap(originalImage, (int)width, (int)height, true);

                var matrix = new Matrix();

                switch (orientation)
                {
                    case "1": // landscape
                        break;
                    case "3":
                        matrix.PreRotate(180);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "4":
                        matrix.PreRotate(180);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "5":
                        matrix.PreRotate(90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "6": // portrait
                        matrix.PreRotate(90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "7":
                        matrix.PreRotate(-90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "8":
                        matrix.PreRotate(-90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                }

                byte[] imageBytes;

                using (MemoryStream ms = new MemoryStream())
                {
                    scaledImage.Compress(Bitmap.CompressFormat.Jpeg, imageCompression, ms);
                    imageBytes = ms.ToArray();
                    await File.WriteAllBytesAsync(path, imageBytes);
                }

                originalImage.Recycle();
                scaledImage.Recycle();
                originalImage.Dispose();
                scaledImage.Dispose();

                return imageBytes;
            }

            catch (IOException ex)
            {
                _ = ex.Message;
                return null;
            }
        }
    }
glintpursuit commented 3 years ago

I have this combined with MediaPicker and it works fine, I have not done the iOS one yet. It sure can be improved.

 public class PhotoPickerService : IPhotoPickerService
    {
        public async Task<byte[]> ImageToArrayAsync(string path)
        {
            try
            {
                var exif = new Android.Media.ExifInterface(path);
                string orientation = exif.GetAttribute(Android.Media.ExifInterface.TagOrientation);

                //Get the bitmap.
                var originalImage = BitmapFactory.DecodeFile(path);

                //Set imageSize and imageCompression parameters.
                var imageSize = .40;
                var imageCompression = 45;

                //Resize it and then compress it to Jpeg.
                var width = originalImage.Width * imageSize;
                var height = originalImage.Height * imageSize;
                var scaledImage = Bitmap.CreateScaledBitmap(originalImage, (int)width, (int)height, true);

                var matrix = new Matrix();

                switch (orientation)
                {
                    case "1": // landscape
                        break;
                    case "3":
                        matrix.PreRotate(180);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "4":
                        matrix.PreRotate(180);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "5":
                        matrix.PreRotate(90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "6": // portrait
                        matrix.PreRotate(90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "7":
                        matrix.PreRotate(-90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "8":
                        matrix.PreRotate(-90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                }

                byte[] imageBytes;

                using (MemoryStream ms = new MemoryStream())
                {
                    scaledImage.Compress(Bitmap.CompressFormat.Jpeg, imageCompression, ms);
                    imageBytes = ms.ToArray();
                    await File.WriteAllBytesAsync(path, imageBytes);
                }

                originalImage.Recycle();
                scaledImage.Recycle();
                originalImage.Dispose();
                scaledImage.Dispose();

                return imageBytes;
            }

            catch (IOException ex)
            {
                _ = ex.Message;
                return null;
            }
        }
    }

iOS : https://github.com/xamarin/Essentials/issues/1514#issuecomment-874148026 this one can be improved too #1078

angelru commented 3 years ago

Tengo esto combinado con MediaPicker y funciona bien, todavía no he hecho el de iOS. Seguro que se puede mejorar.

 public class PhotoPickerService : IPhotoPickerService
    {
        public async Task<byte[]> ImageToArrayAsync(string path)
        {
            try
            {
                var exif = new Android.Media.ExifInterface(path);
                string orientation = exif.GetAttribute(Android.Media.ExifInterface.TagOrientation);

                //Get the bitmap.
                var originalImage = BitmapFactory.DecodeFile(path);

                //Set imageSize and imageCompression parameters.
                var imageSize = .40;
                var imageCompression = 45;

                //Resize it and then compress it to Jpeg.
                var width = originalImage.Width * imageSize;
                var height = originalImage.Height * imageSize;
                var scaledImage = Bitmap.CreateScaledBitmap(originalImage, (int)width, (int)height, true);

                var matrix = new Matrix();

                switch (orientation)
                {
                    case "1": // landscape
                        break;
                    case "3":
                        matrix.PreRotate(180);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "4":
                        matrix.PreRotate(180);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "5":
                        matrix.PreRotate(90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "6": // portrait
                        matrix.PreRotate(90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "7":
                        matrix.PreRotate(-90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                    case "8":
                        matrix.PreRotate(-90);
                        scaledImage = Bitmap.CreateBitmap(scaledImage, 0, 0, scaledImage.Width, scaledImage.Height, matrix, true);
                        matrix.Dispose();
                        matrix = null;
                        break;
                }

                byte[] imageBytes;

                using (MemoryStream ms = new MemoryStream())
                {
                    scaledImage.Compress(Bitmap.CompressFormat.Jpeg, imageCompression, ms);
                    imageBytes = ms.ToArray();
                    await File.WriteAllBytesAsync(path, imageBytes);
                }

                originalImage.Recycle();
                scaledImage.Recycle();
                originalImage.Dispose();
                scaledImage.Dispose();

                return imageBytes;
            }

            catch (IOException ex)
            {
                _ = ex.Message;
                return null;
            }
        }
    }

iOS: # 1514 (comentario) este también se puede mejorar # 1078

The reason for my code is that if an image caught with MediaPicker I apply a Resize, Compress, it ends up rotated.

dimonovdd commented 3 years ago

But in this case you have no chance because the exif data is lost. @stoff99 The bug is not with the rotated image. The bug is that we don't know that the image is rotated.

I believe that the image should not be processed by Essentials in any way. The method should just return a original image without any changes and with meta data.

stoff99 commented 3 years ago

@dimonovdd its converted from the original to png or jpg. I'm not sure right now but its converted and in this case its loosing the meta data. Check this comment above: https://github.com/xamarin/Essentials/issues/1514#issuecomment-768087766

Yarostza commented 3 years ago

Bump

henrikweimenhog commented 3 years ago

I have implemented custom dependency, you will start getting metadata of image

public class MediaPicker : IMediaPicker
    {      
        public async Task<FileResult> CapturePhotoAsync(MediaPickerOptions options)
        {
            TaskCompletionSource<FileResult> taskCompletionSource = new TaskCompletionSource<FileResult>();

            if (await Permissions.RequestAsync<Permissions.Camera>() == PermissionStatus.Granted)
            {
                //Create an image picker object
                UIImagePickerController imagePicker = new()
                {
                    SourceType = UIImagePickerControllerSourceType.Camera,
                    MediaTypes = new string[] { UTType.Image },

                };

                if (string.IsNullOrEmpty(options.Title))
                {
                    imagePicker.Title = options.Title;
                }

                var vc = Platform.GetCurrentUIViewController();

                imagePicker.AllowsEditing = false;
                await vc.PresentViewControllerAsync(imagePicker, true);
                imagePicker.FinishedPickingMedia += async(object sender, UIImagePickerMediaPickedEventArgs e) =>
                {
                    UIImage uimage = e.Info[UIImagePickerController.OriginalImage] as UIImage;

                    var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
                    string jpgFilename = System.IO.Path.Combine(documentsDirectory, System.Guid.NewGuid() + ".jpg");
                    NSData imgData = uimage.AsJPEG();

                    NSError err = null;

                    await vc.DismissViewControllerAsync(true);

                    if (imgData.Save(jpgFilename, false, out err))
                    {

                        taskCompletionSource.TrySetResult(new FileResult(jpgFilename));
                    }
                    else
                    {
                        taskCompletionSource.TrySetException(new Exception("Unable to save the image" + err?.ToString()));
                    }

                    imagePicker?.Dispose();
                    imagePicker = null;
                };

                imagePicker.Canceled += async(object sender, EventArgs e) =>
                {
                    await vc.DismissViewControllerAsync(true);
                    taskCompletionSource.TrySetResult(null);
                    imagePicker?.Dispose();
                    imagePicker = null;
                };
            }
            else
            {
                taskCompletionSource.TrySetException(new PermissionException("Camera permission not granted"));
            }

            return await taskCompletionSource.Task;
        }

        class CameraDelegate : UIImagePickerControllerDelegate
        {
            public override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info)
            {
                picker.DismissModalViewController(true);                
            }
        }       
    }

This looks great but what about IMediaPicker interface? should I create that on my own or should it get it from Essentials?