Forráskód Böngészése

Replaced SkiaSharp Image Processing with Platform-Specific code

Frank van den Bos 1 éve
szülő
commit
f90ef71fbc

+ 173 - 25
InABox.Mobile/InABox.Mobile.Android/ImageToolsDroid.cs

@@ -1,14 +1,15 @@
 using System;
-using System.Collections.Generic;
 using System.Drawing;
 using System.IO;
-using Android.Content;
+using System.Threading.Tasks;
 using Android.Graphics;
 using Android.Media;
 using Java.IO;
+using Xamarin.Essentials;
 using Bitmap = Android.Graphics.Bitmap;
 using File = System.IO.File;
 using Path = System.IO.Path;
+using Stream = System.IO.Stream;
 
 [assembly: Xamarin.Forms.Dependency(typeof(InABox.Mobile.Android.ImageToolsDroid))]
 namespace InABox.Mobile.Android
@@ -16,7 +17,7 @@ namespace InABox.Mobile.Android
     public class ImageToolsDroid: IImageTools
     {
 
-        public byte[] CreateVideoThumbnail(byte[] video, float maxwidth, float maxheight)
+        public byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight)
         {
             byte[] result = null;
             var filename = Path.Combine(
@@ -32,48 +33,195 @@ namespace InABox.Mobile.Android
                 Bitmap bitmap = retriever.GetFrameAtTime(0);
                 if (bitmap != null)
                 {
+                    bitmap = ScaleImage(bitmap, new Size(maxwidth, maxheight));
                     MemoryStream stream = new MemoryStream();
-                    bitmap.Compress(Bitmap.CompressFormat.Png, 0, stream);
+                    bitmap.Compress(Bitmap.CompressFormat.Png, 60, stream);
                     result = stream.ToArray();
                 }
             }
             File.Delete(filename);
-            if (result != null)
-                result = CreateThumbnail(result, maxwidth, maxheight);
             return result;
         }
         
-        public byte[] CreateThumbnail(byte[] source, float maxwidth, float maxheight)
+        public byte[] CreateThumbnail(byte[] source, int maxwidth, int maxheight)
         {
+            return ScaleImage(source, new Size(maxwidth, maxheight), 60);
+        }
+        
+        public enum ImageOrientation
+        {
+            Undefined = 0,
+            Normal = 1,
+            FlipHorizontal = 2,
+            Rotate180 = 3,
+            FlipVertical = 4,
+            Transpose = 5,
+            Rotate90 = 6,
+            Transverse = 7,
+            Rotate270 = 8
+        }
+        
+        public async Task<FileResult> PickPhotoAsync(int? compression, Size? constraints)
+        {
+            var fileResult = await MediaPicker.CapturePhotoAsync();
+            if (fileResult == null)
+                return null;
+            return await ProcessFile(fileResult, compression, constraints);
+        }
+        
+        public async Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints)
+        {
+            var fileResult = await MediaPicker.CapturePhotoAsync();
+            if (fileResult == null)
+                return null;
+            return await ProcessFile(fileResult, compression, constraints);
+        }
+
+        private async Task<FileResult> ProcessFile(FileResult fileResult, int? compression, Size? constraints)
+        {
+            await using var stream = await fileResult.OpenReadAsync();
+
+            var orientation = GetImageOrientation(stream);
+
+            var source = await BitmapFactory.DecodeStreamAsync(stream);
+            var rotated = RotateImage(source, orientation);
+
+            var scaled = ScaleImage(rotated, constraints);
+
+            var jpegFilename = Path.Combine(FileSystem.CacheDirectory, $"{Guid.NewGuid()}.jpg");
+            using (var outStream = new MemoryStream())
+            {
+                await scaled.CompressAsync(Bitmap.CompressFormat.Jpeg, compression ?? 100, outStream);
+                outStream.Position = 0;
+                await File.WriteAllBytesAsync(jpegFilename, outStream.ToArray());
+            }
+
+            return new FileResult(jpegFilename);
+        }
+
+        private static Bitmap RotateImage(Bitmap source, ImageOrientation orientation)
+        {
+            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;
+            }
+
+            return Bitmap.CreateBitmap(
+                source,
+                0,
+                0,
+                source.Width,
+                source.Height,
+                matrix,
+                true);
+        }
+
+        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();
+            stream.Position = 0;
+            return orientation;
+        }
+
+        private static Bitmap ScaleImage(Bitmap source, Size? constraints)
+        {
+            var maxwidth = constraints?.Width ?? source.Width;
+            var maxheight = constraints?.Height ?? source.Height;
+            var wRatio = maxwidth < source.Width
+                ? maxwidth / (double)source.Width
+                : 1.0F;
+            var hRatio = maxheight < source.Height
+                ? maxheight / (double)source.Height
+                : 1.0F;
+            var ratio = Math.Min(hRatio, wRatio);
+            var result = (ratio < 1.0F)
+                ? Bitmap.CreateScaledBitmap(
+                    source,
+                    (int)(source.Width * ratio),
+                    (int)(source.Height * ratio),
+                    true)
+                : source;
+            return result;
+        }
+
+        public byte[] RotateImage(byte[] source, float angle, int compression = 100)
+        {
+            if (angle % 360 == 0)
+                return source;
             byte[] result = { };
             using (var image = BitmapFactory.DecodeByteArray(source, 0, source.Length))
-            {  
+            {
                 if (image != null)
                 {
-                    var size = new Size((int)image.GetBitmapInfo().Height, (int)image.GetBitmapInfo().Width);
-                    if ((size.Width > 0) && (size.Height > 0))
+                    
+                    var matrix = new Matrix();
+                    matrix.PreRotate(angle);
+                    
+                    var rotated = Bitmap.CreateBitmap(
+                        image,
+                        0,
+                        0,
+                        image.Width,
+                        image.Height,
+                        matrix,
+                        true);
+                    
+                    if (rotated != null)
                     {
-                        var maxFactor = Math.Min(maxwidth / size.Width, maxheight / size.Height);
-                        var width = maxFactor * size.Width;
-                        var height = maxFactor * size.Height;
-                        using (var tgt = Bitmap.CreateScaledBitmap(image, (int)height, (int)width, true))
+                        using (var ms = new MemoryStream())
                         {
-                            if (tgt != null)
-                            {
-                                using (var ms = new MemoryStream())
-                                {
-                                    tgt.Compress(Bitmap.CompressFormat.Jpeg, 95, ms);
-                                    result = ms.ToArray();
-                                }
-
-                                tgt.Recycle();
-                            }
+                            rotated.Compress(Bitmap.CompressFormat.Jpeg, compression, ms);
+                            result = ms.ToArray();
                         }
                     }
-                    image.Recycle();
+                }
+            }
+            return result;
+        }
+
+        public byte[] ScaleImage(byte[] source, Size? constraints, int compression = 100)
+        {
+            byte[] result = { };
+            using (var image = BitmapFactory.DecodeByteArray(source, 0, source.Length))
+            {
+                if (image != null)
+                {
+                    var scaled = ScaleImage(image, constraints);
+                    using (var ms = new MemoryStream())
+                    {
+                        scaled.Compress(Bitmap.CompressFormat.Jpeg, compression, ms);
+                        result = ms.ToArray();
+                    }
                 }
             }
             return result;
         }
     }
+
 }

+ 14 - 2
InABox.Mobile/InABox.Mobile.Shared/IImageTools.cs

@@ -1,10 +1,22 @@
+using System.Drawing;
+using System.Threading.Tasks;
+using Xamarin.Essentials;
+
 namespace InABox.Mobile
 {
     public interface IImageTools
     {
 
-        byte[] CreateVideoThumbnail(byte[] video, float maxwidth, float maxheight);
+        byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight);
+        
+        byte[] CreateThumbnail(byte[] image, int maxwidth, int maxheight);
         
-        byte[] CreateThumbnail(byte[] image, float maxwidth, float maxheight);
+        // Goal - to return a properly rotated, scaled and compressed JPEG Image
+        Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints);
+        Task<FileResult> PickPhotoAsync(int? compression, Size? constraints);
+
+        byte[] RotateImage(byte[] image, float angle, int quality = 100);
+
+        byte[] ScaleImage(byte[] image, Size? constraints, int quality = 100);
     }
 }

+ 0 - 1
InABox.Mobile/InABox.Mobile.Shared/InABox.Mobile.Shared.csproj

@@ -22,7 +22,6 @@
         </PackageReference>
         <PackageReference Include="Serilog" Version="3.0.1" />
         <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
-        <PackageReference Include="SkiaSharp" Version="2.88.6" />
         <PackageReference Include="Syncfusion.Licensing" Version="23.2.4" />
         <PackageReference Include="Syncfusion.Xamarin.Pdf" Version="23.2.4" />
         <PackageReference Include="Syncfusion.Xamarin.SfBusyIndicator" Version="23.2.4" />

+ 1 - 1
InABox.Mobile/InABox.Mobile.Shared/MobileDocument/Images/MobileDocumentCameraSource.cs

@@ -14,7 +14,7 @@ namespace InABox.Mobile
             => await IsEnabled<Permissions.Camera>();
         
         protected override async Task<FileResult> Capture() 
-            => await MediaPicker.CapturePhotoAsync();
+            => await MobileUtils.ImageTools.CapturePhotoAsync(Options.Compression, Options.Constraints);
 
 
     }

+ 2 - 3
InABox.Mobile/InABox.Mobile.Shared/MobileDocument/Images/MobileDocumentPhotoLibrarySource.cs

@@ -14,8 +14,7 @@ namespace InABox.Mobile
             => await IsEnabled<Permissions.Photos>();
 
         protected override async Task<FileResult> Capture() 
-            => await MediaPicker.PickPhotoAsync();
-
-
+            => await MobileUtils.ImageTools.PickPhotoAsync(Options.Compression, Options.Constraints);
+        
     }
 }

+ 4 - 4
InABox.Mobile/InABox.Mobile.Shared/MobileDocument/Images/MobileImageSource.cs

@@ -14,11 +14,11 @@ namespace InABox.Mobile
             if (Options == null)
                 return;
             
-            if (Options.Constraints != null)
-                document.Scale(Options.Constraints.Value);
+            //if (Options.Constraints != null)
+            //    document.Scale(Options.Constraints.Value);
             
-            if (Options.Compression != null && Options.Compression != 100)
-                document.ConvertToJpg(Options.Compression.Value);
+            //if (Options.Compression != null && Options.Compression != 100)
+            //    document.ConvertToJpg(Options.Compression.Value);
             
             if (Options.PDF == true)
                 document.ConvertToPDF();

+ 0 - 12
InABox.Mobile/InABox.Mobile.Shared/MobileDocument/MobileDocument.cs

@@ -51,18 +51,6 @@ namespace InABox.Mobile
 
         }
         
-        public void Scale(Size constraints)
-        {
-            Data = MobileUtils.ScaleImage(Data, constraints.Width, constraints.Height);
-        }
-
-        public void ConvertToJpg(int quality = 60)
-        {
-            Data = MobileUtils.ToJpg(Data, quality);
-            FileName = Path.ChangeExtension(FileName, "jpg");    
-        }
-
-        
         public bool IsPDF() => FileName.ToUpper().EndsWith(".PDF");
 
         

+ 0 - 79
InABox.Mobile/InABox.Mobile.Shared/MobileUtils.cs

@@ -4,8 +4,6 @@ using System.Threading.Tasks;
 using System.Collections.Generic;
 using System.IO;
 using InABox.Core;
-using JetBrains.Annotations;
-using SkiaSharp;
 using Xamarin.Forms;
 using Xamarin.Essentials;
 
@@ -145,82 +143,5 @@ namespace InABox.Mobile
             return s;
         }
         
-        [CanBeNull]
-        public static byte[] RotateImage([CanBeNull] byte[] data)
-        {
-            if (data?.Any() != true)
-                return data;
-
-            SKEncodedImageFormat format;
-            using (var ms = new MemoryStream(data))
-            {
-                using (var codec = SKCodec.Create(ms))
-                    format = codec.EncodedFormat;
-            }
-
-            var source = SKBitmap.Decode(data);
-            var result = new SKBitmap(source.Height, source.Width);
-            using (var surface = new SKCanvas(result))
-            {
-                surface.Translate(result.Width, 0);
-                surface.RotateDegrees(90);
-                surface.DrawBitmap(source, 0, 0);
-            }
-            return result.Encode(format,100).ToArray();
-        }
-        
-        [CanBeNull]
-        public static byte[] ScaleImage([CanBeNull] byte[] data, int maxwidth, int maxheight)
-        {
-            if (data?.Any() != true)
-                return data;
-            
-            SKEncodedImageFormat format;
-            using (var ms = new MemoryStream(data))
-            {
-                using (var codec = SKCodec.Create(ms))
-                    format = codec.EncodedFormat;
-            }
-
-            var source = SKBitmap.Decode(data);
-
-            var wRatio = maxwidth < source.Width
-                ? (double)maxwidth / (double)source.Width
-                : 1.0F;
-            var hRatio = maxheight < source.Height
-                ? (double)maxheight / (double)source.Height
-                : 1.0F;
-            var ratio = Math.Min(hRatio, wRatio);
-            if (ratio > 1)
-                return data;
-            
-            var size = new SKImageInfo((int)(source.Width * ratio), (int)(source.Height * ratio));
-            var result = source.Resize(size, SKFilterQuality.High);
-            return result.Encode(format,100).ToArray();
-        }
-
-        [CanBeNull]
-        public static byte[] ToJpg([CanBeNull] byte[] data, int quality = 60)
-        {
-            if (data?.Any() != true)
-                return data;
-            
-            SKEncodedImageFormat format;
-            using (var ms = new MemoryStream(data))
-            {
-                using (var codec = SKCodec.Create(ms))
-                    format = codec.EncodedFormat;
-            }
-
-            if (format != SKEncodedImageFormat.Jpeg)
-            {
-                var result = SKBitmap.Decode(data);
-                return result.Encode(SKEncodedImageFormat.Jpeg,quality).ToArray();
-            }
-
-            return data;
-        }
-        
-        
     }
 }

+ 171 - 19
InABox.Mobile/InABox.Mobile.iOS/ImageToolsiOS.cs

@@ -1,11 +1,15 @@
 
 using System;
+using System.Drawing;
 using System.IO;
+using System.Threading.Tasks;
 using AVFoundation;
 using CoreGraphics;
 using CoreMedia;
 using Foundation;
 using UIKit;
+using Xamarin.Essentials;
+using MobileCoreServices;
 
 [assembly: Xamarin.Forms.Dependency(typeof(InABox.Mobile.iOS.ImageToolsiOS))]
 namespace InABox.Mobile.iOS
@@ -13,8 +17,7 @@ namespace InABox.Mobile.iOS
     
     public class ImageToolsiOS : IImageTools
     {
-
-        public byte[] CreateVideoThumbnail(byte[] video, float maxwidth, float maxheight)
+        public byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight)
         {
             byte[] result = null;
             var filename = Path.Combine(
@@ -38,39 +41,188 @@ namespace InABox.Mobile.iOS
             }
 
             if (result != null)
-                result = CreateThumbnail(result, maxwidth, maxheight);
+                result = ScaleImage(result, new Size(maxwidth, maxheight), 60);
             
             File.Delete(filename);
             return result;
         }
         
-        public byte[] CreateThumbnail(byte[] source, float maxwidth, float maxheight)
+        public byte[] CreateThumbnail(byte[] source, int maxwidth, int maxheight)
+        {
+            return ScaleImage(source, new Size(maxwidth, maxheight), 60);
+        }
+        
+        public async Task<FileResult> PickPhotoAsync(int? compression, Size? constraints)
+        {
+            return await MainThread.InvokeOnMainThreadAsync(async () =>
+                await InternalGetPhotoAsync<Permissions.Photos>(UIImagePickerControllerSourceType.PhotoLibrary, compression, constraints));
+        }
+        
+         public async Task<FileResult> CapturePhotoAsync(int? compression, Size? constraints)
+        {
+            return await MainThread.InvokeOnMainThreadAsync(async () =>
+                await InternalGetPhotoAsync<Permissions.Camera>(UIImagePickerControllerSourceType.Camera, compression, constraints));
+        }
+
+        private async Task<FileResult> InternalGetPhotoAsync<TPermission>(UIImagePickerControllerSourceType source, int? compression, Size? constraints)
+            where TPermission : Permissions.BasePermission, new()
+        {
+            var taskCompletionSource = new TaskCompletionSource<FileResult>();
+            if (await Permissions.RequestAsync<TPermission>() == PermissionStatus.Granted)
+            {
+                var imagePicker = new UIImagePickerController
+                {
+                    SourceType = source,
+                    MediaTypes = new string[] { UTType.Image }
+                };
+                
+                var viewController = Platform.GetCurrentUIViewController();
+
+                imagePicker.AllowsEditing = false;
+                imagePicker.FinishedPickingMedia += async (sender, e) =>
+                {
+                    var jpegFilename = Path.Combine(FileSystem.CacheDirectory, $"{Guid.NewGuid()}.jpg");
+                    var source = e.Info[UIImagePickerController.OriginalImage] as UIImage;
+                    var rotated = AutoRotateImage(source);
+                    var scaled = ScaleImage(rotated, constraints);
+                    var result = scaled.AsJPEG(new nfloat(compression ?? 100)/100);
+                    await viewController.DismissViewControllerAsync(true);
+                    if (result.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;
+        }
+        
+        private UIImage AutoRotateImage(UIImage source)
+        {
+            var rotation = source.Orientation switch
+            {
+                UIImageOrientation.Right => 90F,
+                UIImageOrientation.Up => 0F,
+                UIImageOrientation.Left => -90F,
+                UIImageOrientation.Down => 180F,
+                _ => 0F
+            };
+
+            return RotateImage(source, rotation);
+        }
+
+        private UIImage RotateImage(UIImage source, float rotation)
+        {
+            CGImage imgRef = source.CGImage;
+            float width = imgRef.Width;
+            float height = imgRef.Height;
+            CGAffineTransform transform = CGAffineTransform.MakeIdentity();
+            RectangleF bounds = new RectangleF(0, 0, width, height);
+
+            float angle = Convert.ToSingle((rotation / 180f) * Math.PI);
+            transform = CGAffineTransform.MakeRotation(angle);
+
+            UIGraphics.BeginImageContext(bounds.Size);
+
+            CGContext context = UIGraphics.GetCurrentContext();
+
+            context.TranslateCTM(width / 2, height / 2);
+            context.SaveState();
+            context.ConcatCTM(transform);
+            context.SaveState();
+            context.ConcatCTM(CGAffineTransform.MakeScale(1.0f, -1.0f));
+
+            context.DrawImage(new RectangleF(-width / 2, -height / 2, width, height), imgRef);
+            context.RestoreState();
+
+            UIImage result = UIGraphics.GetImageFromCurrentImageContext();
+            UIGraphics.EndImageContext();
+            return result;
+        }
+
+        private UIImage ScaleImage(UIImage sourceImage, Size? constraints)
+        {
+            var maxwidth = constraints?.Width ?? sourceImage.Size.Width;
+            var maxheight = constraints?.Height ?? sourceImage.Size.Height;
+            var wRatio = maxwidth < sourceImage.Size.Width
+                ? maxwidth / (double)sourceImage.Size.Width
+                : 1.0F;
+            var hRatio = maxheight < sourceImage.Size.Height
+                ? maxheight / (double)sourceImage.Size.Height
+                : 1.0F;
+            var ratio = Math.Min(hRatio, wRatio);
+            if (ratio < 1.0F)
+            {
+                var width = ratio * sourceImage.Size.Width;
+                var height = ratio * sourceImage.Size.Height;
+                UIGraphics.BeginImageContext(new CGSize(width, height));
+                sourceImage.Draw(new CGRect(0, 0, width, height));
+                var resultImage = UIGraphics.GetImageFromCurrentImageContext();
+                UIGraphics.EndImageContext();
+                return resultImage;
+            }
+            return sourceImage;
+        }
+
+        public byte[] RotateImage(byte[] source, float angle, int quality = 100)
         {
             byte[] result = { };
             using (UIImage src = UIImage.LoadFromData(NSData.FromArray(source)))
             {
                 if (src != null)
                 {
-                    if ((src.Size.Width > 0.0F) && (src.Size.Height > 0.0F))
+                    var scaled = RotateImage(src, angle);
+                    using (NSData imageData = scaled.AsJPEG(new nfloat((float)quality / 100F)))
                     {
-                        var maxFactor = Math.Min(maxwidth / src.Size.Width, maxheight / src.Size.Height);
-                        var width = maxFactor * src.Size.Width;
-                        var height = maxFactor * src.Size.Height;
-                        UIGraphics.BeginImageContextWithOptions(new CGSize((float)width, (float)height), true, 1.0f);
-                        src.Draw(new CGRect(0, 0, (float)width, (float)height));
-                        var tgt = UIGraphics.GetImageFromCurrentImageContext();
-                        UIGraphics.EndImageContext();
-
-                        using (NSData imageData = tgt.AsJPEG())
-                        {
-                            result = new byte[imageData.Length];
-                            System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, result, 0,
-                                Convert.ToInt32(imageData.Length));
-                        }
+                        result = new byte[imageData.Length];
+                        System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, result, 0,
+                            Convert.ToInt32(imageData.Length));
+                    }
+                }
+            }
+            return result;
+        }
+
+        public byte[] ScaleImage(byte[] source, Size? constraints, int quality = 100)
+        {
+            byte[] result = { };
+            using (UIImage src = UIImage.LoadFromData(NSData.FromArray(source)))
+            {
+                if (src != null)
+                {
+                    var scaled = ScaleImage(src, constraints);
+                    using (NSData imageData = scaled.AsJPEG(new nfloat((float)quality / 100F)))
+                    {
+                        result = new byte[imageData.Length];
+                        System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, result, 0,
+                            Convert.ToInt32(imageData.Length));
                     }
                 }
             }
             return result;
         }
     }
+
+    
 }