ImageTools.iOS.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. using System;
  2. using System.Drawing;
  3. using System.IO;
  4. using System.Runtime.InteropServices;
  5. using System.Threading.Tasks;
  6. using Avalonia.Controls;
  7. using AVFoundation;
  8. using CoreGraphics;
  9. using CoreMedia;
  10. using Foundation;
  11. using InABox.Core;
  12. using Microsoft.Maui.ApplicationModel;
  13. using Microsoft.Maui.Storage;
  14. using MobileCoreServices;
  15. using UIKit;
  16. namespace InABox.Avalonia.Platform.iOS
  17. {
  18. public class iOS_ImageTools : IImageTools
  19. {
  20. public Logger Logger { get; set; }
  21. public byte[] CreateVideoThumbnail(byte[] video, int maxwidth, int maxheight)
  22. {
  23. byte[] result = null;
  24. var filename = Path.Combine(
  25. Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
  26. $"{Guid.NewGuid().ToString()}.tmp"
  27. );
  28. File.WriteAllBytes(filename,video);
  29. // perhaps? on IOS, try to use - NSUrl.FromFilename (path) instead of - new Foundation.NSUrl(url)
  30. var asset = AVAsset.FromUrl(NSUrl.FromFilename(filename));
  31. AVAssetImageGenerator imageGenerator = new AVAssetImageGenerator(asset);
  32. //AVAssetImageGenerator imageGenerator = new AVAssetImageGenerator(AVAsset.FromUrl((new Foundation.NSUrl(filename))));
  33. imageGenerator.AppliesPreferredTrackTransform = true;
  34. CMTime actualTime;
  35. NSError error;
  36. CGImage cgImage = imageGenerator.CopyCGImageAtTime(new CMTime(1, 1), out actualTime, out error);
  37. using (var ms = new MemoryStream())
  38. {
  39. var png = new UIImage(cgImage).AsPNG().AsStream();
  40. png.CopyTo(ms);
  41. result = ms.ToArray();
  42. }
  43. if (result != null)
  44. result = ScaleImage(result, new Size(maxwidth, maxheight), 60);
  45. File.Delete(filename);
  46. return result;
  47. }
  48. public byte[] CreateThumbnail(byte[] source, int maxwidth, int maxheight)
  49. {
  50. return ScaleImage(source, new Size(maxwidth, maxheight), 60);
  51. }
  52. public async Task<ImageFile?> PickPhotoAsync(TopLevel window, int? compression, Size? constraints)
  53. {
  54. return await MainThread.InvokeOnMainThreadAsync(async () =>
  55. await InternalGetPhotoAsync<Permissions.Photos>(UIImagePickerControllerSourceType.PhotoLibrary, compression, constraints));
  56. }
  57. public async Task<ImageFile?> CapturePhotoAsync(TopLevel window, int? compression, Size? constraints)
  58. {
  59. return await MainThread.InvokeOnMainThreadAsync(async () =>
  60. await InternalGetPhotoAsync<Permissions.Camera>(UIImagePickerControllerSourceType.Camera, compression, constraints));
  61. }
  62. public async Task<ImageFile?> PickVideoAsync(TopLevel window)
  63. {
  64. return await MainThread.InvokeOnMainThreadAsync(async () =>
  65. await InternalGetPhotoAsync<Permissions.Photos>(UIImagePickerControllerSourceType.PhotoLibrary, null, null));
  66. }
  67. public async Task<ImageFile?> CaptureVideoAsync(TopLevel window)
  68. {
  69. return await MainThread.InvokeOnMainThreadAsync(async () =>
  70. await InternalGetPhotoAsync<Permissions.Camera>(UIImagePickerControllerSourceType.Camera, null, null));
  71. }
  72. private async Task<ImageFile> InternalGetPhotoAsync<TPermission>(UIImagePickerControllerSourceType source, int? compression, Size? constraints)
  73. where TPermission : Permissions.BasePermission, new()
  74. {
  75. var taskCompletionSource = new TaskCompletionSource<ImageFile>();
  76. if (await Permissions.RequestAsync<TPermission>() == PermissionStatus.Granted)
  77. {
  78. var imagePicker = new UIImagePickerController
  79. {
  80. SourceType = source,
  81. MediaTypes = new string[] { UTType.Image }
  82. };
  83. // Struggling to get IOS define to enable Platform shared code, which is a bit shitty
  84. var viewController = ViewControllerHelper.GetVisibleViewController();
  85. imagePicker.AllowsEditing = false;
  86. imagePicker.FinishedPickingMedia += async (sender, e) =>
  87. {
  88. var jpegFilename = Path.Combine(FileSystem.CacheDirectory, $"{Guid.NewGuid()}.jpg");
  89. var source = e.Info[UIImagePickerController.OriginalImage] as UIImage;
  90. var rotated = AutoRotateImage(source);
  91. var scaled = ScaleImage(rotated, constraints);
  92. var result = scaled.AsJPEG(new NFloat(compression ?? 100)/100);
  93. await viewController.DismissViewControllerAsync(true);
  94. if (result.Save(jpegFilename, false, out var error))
  95. {
  96. taskCompletionSource.TrySetResult(new ImageFile(jpegFilename, result.ToArray()));
  97. }
  98. else
  99. {
  100. taskCompletionSource.TrySetException(new Exception($"Error saving the image: {error}"));
  101. }
  102. imagePicker?.Dispose();
  103. imagePicker = null;
  104. };
  105. imagePicker.Canceled += async (sender, e) =>
  106. {
  107. await viewController.DismissViewControllerAsync(true);
  108. taskCompletionSource.TrySetResult(null);
  109. imagePicker?.Dispose();
  110. imagePicker = null;
  111. };
  112. await viewController.PresentViewControllerAsync(imagePicker, true);
  113. }
  114. else
  115. {
  116. taskCompletionSource.TrySetResult(null);
  117. taskCompletionSource.TrySetException(new PermissionException("Camera permission not granted"));
  118. }
  119. return await taskCompletionSource.Task;
  120. }
  121. private UIImage AutoRotateImage(UIImage source)
  122. {
  123. var rotation = source.Orientation switch
  124. {
  125. UIImageOrientation.Right => 90F,
  126. UIImageOrientation.Up => 0F,
  127. UIImageOrientation.Left => -90F,
  128. UIImageOrientation.Down => 180F,
  129. _ => 0F
  130. };
  131. return RotateImage(source, rotation);
  132. }
  133. private UIImage RotateImage(UIImage source, float rotation)
  134. {
  135. UIImage imageToReturn = null;
  136. float radians = -1 * ((float)(rotation * Math.PI) / 180);
  137. bool isLandscape = false;
  138. var x = source.Size.Width / 2;
  139. var y = source.Size.Height / 2;
  140. //https://stackoverflow.com/a/8536553
  141. CGAffineTransform transform = new CGAffineTransform((nfloat)Math.Cos(radians), (nfloat)Math.Sin(radians), -(nfloat)Math.Sin(radians), (nfloat)Math.Cos(radians), (nfloat)(x - x * Math.Cos(radians)) + (nfloat)(y * Math.Sin(radians)), (nfloat)(y - x * Math.Sin(radians) - y * Math.Cos(radians)));
  142. var diff = (source.Size.Height - source.Size.Width) / 2;
  143. bool translateWidthAndHeight = false;
  144. if (rotation == 90)
  145. {
  146. translateWidthAndHeight = true;
  147. transform.Translate(diff, -diff);
  148. }
  149. else if (rotation == 180)
  150. {
  151. //Transform.Translate(image.Size.Width, -image.Size.Height);
  152. }
  153. else if (rotation == 270)
  154. {
  155. translateWidthAndHeight = true;
  156. transform.Translate(diff, -diff);
  157. }
  158. else if (rotation == 360)
  159. {
  160. }
  161. if (translateWidthAndHeight)
  162. {
  163. //now draw image
  164. using (var context = new CGBitmapContext(IntPtr.Zero,
  165. (int)source.Size.Height,
  166. (int)source.Size.Width,
  167. source.CGImage.BitsPerComponent,
  168. source.CGImage.BitsPerComponent * (int)source.Size.Width,
  169. source.CGImage.ColorSpace,
  170. source.CGImage.BitmapInfo))
  171. {
  172. context.ConcatCTM(transform);
  173. context.DrawImage(new RectangleF(PointF.Empty, new SizeF((float)source.Size.Width, (float)source.Size.Height)), source.CGImage);
  174. using (var imageRef = context.ToImage())
  175. {
  176. imageToReturn = new UIImage(imageRef);
  177. }
  178. }
  179. }
  180. else
  181. {
  182. //now draw image
  183. using (var context = new CGBitmapContext(IntPtr.Zero,
  184. (int)source.Size.Width,
  185. (int)source.Size.Height,
  186. source.CGImage.BitsPerComponent,
  187. source.CGImage.BitsPerComponent * (int)source.Size.Height,
  188. source.CGImage.ColorSpace,
  189. source.CGImage.BitmapInfo))
  190. {
  191. context.ConcatCTM(transform);
  192. context.DrawImage(new RectangleF(PointF.Empty, new SizeF((float)source.Size.Width, (float)source.Size.Height)), source.CGImage);
  193. using (var imageRef = context.ToImage())
  194. {
  195. imageToReturn = new UIImage(imageRef);
  196. }
  197. }
  198. }
  199. return imageToReturn;
  200. }
  201. private UIImage ScaleImage(UIImage sourceImage, Size? constraints)
  202. {
  203. var maxwidth = constraints?.Width ?? sourceImage.Size.Width;
  204. var maxheight = constraints?.Height ?? sourceImage.Size.Height;
  205. var wRatio = maxwidth < sourceImage.Size.Width
  206. ? maxwidth / (double)sourceImage.Size.Width
  207. : 1.0F;
  208. var hRatio = maxheight < sourceImage.Size.Height
  209. ? maxheight / (double)sourceImage.Size.Height
  210. : 1.0F;
  211. var ratio = Math.Min(hRatio, wRatio);
  212. if (ratio < 1.0F)
  213. {
  214. var width = ratio * sourceImage.Size.Width;
  215. var height = ratio * sourceImage.Size.Height;
  216. UIGraphics.BeginImageContext(new CGSize(width, height));
  217. sourceImage.Draw(new CGRect(0, 0, width, height));
  218. var resultImage = UIGraphics.GetImageFromCurrentImageContext();
  219. UIGraphics.EndImageContext();
  220. return resultImage;
  221. }
  222. return sourceImage;
  223. }
  224. public byte[] RotateImage(byte[] source, float angle, int quality = 100)
  225. {
  226. byte[] result = { };
  227. using (UIImage src = UIImage.LoadFromData(NSData.FromArray(source)))
  228. {
  229. if (src != null)
  230. {
  231. var scaled = RotateImage(src, angle);
  232. using (NSData imageData = scaled.AsJPEG(new nfloat((float)quality / 100F)))
  233. {
  234. result = new byte[imageData.Length];
  235. System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, result, 0,
  236. Convert.ToInt32(imageData.Length));
  237. }
  238. }
  239. }
  240. return result;
  241. }
  242. public byte[] ScaleImage(byte[] source, Size? constraints, int quality = 100)
  243. {
  244. byte[] result = { };
  245. using (UIImage src = UIImage.LoadFromData(NSData.FromArray(source)))
  246. {
  247. if (src != null)
  248. {
  249. var scaled = ScaleImage(src, constraints);
  250. using (NSData imageData = scaled.AsJPEG(new nfloat((float)quality / 100F)))
  251. {
  252. result = new byte[imageData.Length];
  253. System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, result, 0,
  254. Convert.ToInt32(imageData.Length));
  255. }
  256. }
  257. }
  258. return result;
  259. }
  260. }
  261. }