When I Capture a Photo with the following code , I get the saved photo rotated 90 degree in Android and iOS
is there any solution for this issue? so I get the saved image orientation adjusted properly
FileResult photo = await MediaPicker.CapturePhotoAsync();
using Stream sourceStream = await photo.OpenReadAsync();
var picture = PlatformImage.FromStream(sourceStream);
string localFilePath = $"{FileSystem.CacheDirectory}/{photo.FileName}";
using FileStream localFileStream = File.OpenWrite(localFilePath);
await picture.SaveAsync(localFileStream, ImageFormat.Jpeg, quality: 0.90f);
The way I handled this was to implement my own CapturePhotoAsync for both iOS and Android so that the image is always being returned the right way up (using some of the code on this thread: https://github.com/xamarin/Essentials/issues/1514). Then I can happily use PlatformImage.FromStream knowing that when I lose the exif data, the image is already the right way up anyway. It should also minimize changes once they fix this issue too.
I created the following interface:
public interface IImageService
{
Task<FileResult> CapturePhotoAsync(MediaPickerOptions options = null);
}
Then under the Platforms/Android directory I created:
using Android.Graphics;
using AndroidX.ExifInterface.Media;
using Microsoft.Extensions.Logging;
using Path = System.IO.Path;
using Stream = System.IO.Stream;
namespace CaptureImageFix
{
public enum ImageOrientation
{
Undefined = 0,
Normal = 1,
FlipHorizontal = 2,
Rotate180 = 3,
FlipVertical = 4,
Transpose = 5,
Rotate90 = 6,
Transverse = 7,
Rotate270 = 8
}
public partial class ImageService : IImageService
{
private readonly ILogger<ImageService> _logger;
public ImageService(ILogger<ImageService> logger)
{
_logger = logger;
}
public async Task<FileResult> CapturePhotoAsync(MediaPickerOptions options = null)
{
var fileResult = await MediaPicker.Default.CapturePhotoAsync(options);
if (fileResult == null)
return null;
await using var stream = await fileResult.OpenReadAsync();
stream.Position = 0;
var orientation = GetImageOrientation(stream);
stream.Position = 0;
var originalBitmap = await BitmapFactory.DecodeStreamAsync(stream);
var matrix = new Matrix();
switch (orientation)
{
case ImageOrientation.Normal:
break;
case ImageOrientation.FlipHorizontal:
break;
case ImageOrientation.Rotate180:
break;
case ImageOrientation.FlipVertical:
matrix.PreRotate(180);
break;
case ImageOrientation.Transpose:
matrix.PreRotate(90);
break;
case ImageOrientation.Rotate90:
matrix.PreRotate(90);
break;
case ImageOrientation.Transverse:
matrix.PreRotate(-90);
break;
case ImageOrientation.Rotate270:
matrix.PreRotate(-90);
break;
}
var normalizedBitmap = Bitmap.CreateBitmap(
originalBitmap,
0,
0,
originalBitmap.Width,
originalBitmap.Height,
matrix,
true);
using var outStream = new MemoryStream();
await normalizedBitmap.CompressAsync(Bitmap.CompressFormat.Jpeg, 100, outStream);
outStream.Position = 0;
var jpegFilename = Path.Combine(FileSystem.CacheDirectory, $"{Guid.NewGuid()}.jpg");
await File.WriteAllBytesAsync(jpegFilename, outStream.ToArray());
return new FileResult(jpegFilename);
}
private ImageOrientation GetImageOrientation(Stream stream)
{
var exif = new ExifInterface(stream);
var tag = exif.GetAttribute(ExifInterface.TagOrientation);
var orientation = string.IsNullOrEmpty(tag) ?
ImageOrientation.Undefined :
(ImageOrientation)Enum.Parse(typeof(ImageOrientation), tag);
exif.Dispose();
return orientation;
}
}
}
And under the Platforms/iOS directory I created (note these are both partial classes and must be in the same namespace):
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Graphics.Platform;
using MobileCoreServices;
using UIKit;
namespace CaptureImageFix
{
public partial class ImageService : IImageService
{
private readonly ILogger<ImageService> _logger;
public ImageService(ILogger<ImageService> logger)
{
_logger = logger;
}
public async Task<FileResult> CapturePhotoAsync(MediaPickerOptions options = null)
{
return await MainThread.InvokeOnMainThreadAsync(async () =>
await InternalCapturePhotoAsync(options));
}
private async Task<FileResult> InternalCapturePhotoAsync(MediaPickerOptions options)
{
var taskCompletionSource = new TaskCompletionSource<FileResult>();
if (await Permissions.RequestAsync<Permissions.Camera>() == PermissionStatus.Granted)
{
var imagePicker = new UIImagePickerController
{
SourceType = UIImagePickerControllerSourceType.Camera,
MediaTypes = new string[] { UTType.Image }
};
if (!string.IsNullOrEmpty(options?.Title))
{
imagePicker.Title = options.Title;
}
var viewController = Platform.GetCurrentUIViewController();
imagePicker.AllowsEditing = false;
imagePicker.FinishedPickingMedia += async (sender, e) =>
{
var jpegFilename = Path.Combine(FileSystem.CacheDirectory, $"{Guid.NewGuid()}.jpg");
var uiImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;
var normalizedImage = uiImage.NormalizeOrientation();
var normalizedData = normalizedImage.AsJPEG(new nfloat(1));
await viewController.DismissViewControllerAsync(true);
if (normalizedData.Save(jpegFilename, false, out var error))
{
taskCompletionSource.TrySetResult(new FileResult(jpegFilename));
}
else
{
taskCompletionSource.TrySetException(new Exception($"Error saving the image: {error}"));
}
imagePicker?.Dispose();
imagePicker = null;
};
imagePicker.Canceled += async (sender, e) =>
{
await viewController.DismissViewControllerAsync(true);
taskCompletionSource.TrySetResult(null);
imagePicker?.Dispose();
imagePicker = null;
};
await viewController.PresentViewControllerAsync(imagePicker, true);
}
else
{
taskCompletionSource.TrySetResult(null);
taskCompletionSource.TrySetException(new PermissionException("Camera permission not granted"));
}
return await taskCompletionSource.Task;
}
}
}
Then in your MauiProgram.cs file add the following:
builder.Services.AddTransient<IImageService, ImageService>();
You can then inject the IImageService and use that to pick an image.
Each implementation overcomes specific issues with the native implementations of MediaPicker.Default.CapturePhotoAsync provided by MAUI:
UIImagePickerController and retrieves the image, corrects it, saves it as a jpeg, and then returns a FileResult pointing to that JPEG.PlatformImage.FromStream is made it loses that data, so to nullify the effect of that, the implementation above wraps the call to MediaPicker.Default.CapturePhotoAsync and then uses the exif data to create a new corrected version of the file.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With