Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ImageCropperView.cs
1using System;
2using System.IO;
3using System.Net.Http;
4using System.Threading;
5using System.Threading.Tasks;
6using Microsoft.Maui.Controls;
7using SkiaSharp;
8using SkiaSharp.Views.Maui;
9using SkiaSharp.Views.Maui.Controls;
10
12{
13 #region Enums
14
18 public enum CropMode
19 {
23 Square,
24
28 Circle,
29
33 Aspect
34 }
35
39 public enum CropOutputFormat
40 {
44 Png,
45
49 Jpeg
50 }
51
52 #endregion
53
59 public class ImageCropperView : ContentView
60 {
61#warning Replace this with InternetContent
65 private static readonly HttpClient sHttpClient = new HttpClient();
66
67 #region Bindable Properties
68
72 public static readonly BindableProperty OutputMaxResolutionProperty = BindableProperty.Create(
73 propertyName: nameof(OutputMaxResolution),
74 returnType: typeof(Size),
75 declaringType: typeof(ImageCropperView),
76 defaultValue: new Size(1280, 1280));
77
82 {
83 get => (Size)this.GetValue(OutputMaxResolutionProperty);
84 set => this.SetValue(OutputMaxResolutionProperty, value);
85 }
86
90 public static readonly BindableProperty InputMaxResolutionProperty = BindableProperty.Create(
91 propertyName: nameof(InputMaxResolution),
92 returnType: typeof(Size),
93 declaringType: typeof(ImageCropperView),
94 defaultValue: new Size(3840, 2160));
95
100 {
101 get => (Size)this.GetValue(InputMaxResolutionProperty);
102 set => this.SetValue(InputMaxResolutionProperty, value);
103 }
104
108 public static readonly BindableProperty ImageSourceProperty = BindableProperty.Create(
109 propertyName: nameof(ImageSource),
110 returnType: typeof(ImageSource),
111 declaringType: typeof(ImageCropperView),
112 defaultValue: null,
113 propertyChanged: OnImageSourceChanged);
114
118 public ImageSource ImageSource
119 {
120 get => (ImageSource)this.GetValue(ImageSourceProperty);
121 set => this.SetValue(ImageSourceProperty, value);
122 }
123
127 public static readonly BindableProperty CropModeProperty = BindableProperty.Create(
128 propertyName: nameof(CropMode),
129 returnType: typeof(CropMode),
130 declaringType: typeof(ImageCropperView),
131 defaultValue: CropMode.Square);
132
137 {
138 get => (CropMode)this.GetValue(CropModeProperty);
139 set => this.SetValue(CropModeProperty, value);
140 }
141
145 public static readonly BindableProperty OutputFormatProperty = BindableProperty.Create(
146 propertyName: nameof(OutputFormat),
147 returnType: typeof(CropOutputFormat),
148 declaringType: typeof(ImageCropperView),
149 defaultValue: CropOutputFormat.Png);
150
155 {
156 get => (CropOutputFormat)this.GetValue(OutputFormatProperty);
157 set => this.SetValue(OutputFormatProperty, value);
158 }
159
163 public static readonly BindableProperty JpegQualityProperty = BindableProperty.Create(
164 propertyName: nameof(JpegQuality),
165 returnType: typeof(int),
166 declaringType: typeof(ImageCropperView),
167 defaultValue: 90);
168
172 public int JpegQuality
173 {
174 get => (int)this.GetValue(JpegQualityProperty);
175 set => this.SetValue(JpegQualityProperty, value);
176 }
177
181 public static readonly BindableProperty CropShapeFillPortionProperty = BindableProperty.Create(
182 propertyName: nameof(CropShapeFillPortion),
183 returnType: typeof(float),
184 declaringType: typeof(ImageCropperView),
185 defaultValue: 0.9f);
186
191 {
192 get => (float)this.GetValue(CropShapeFillPortionProperty);
193 set => this.SetValue(CropShapeFillPortionProperty, value);
194 }
195
199 public static readonly BindableProperty RotationAngleProperty = BindableProperty.Create(
200 propertyName: nameof(RotationAngle),
201 returnType: typeof(double),
202 declaringType: typeof(ImageCropperView),
203 defaultValue: 0.0,
204 propertyChanged: OnRotationAngleChanged);
205
209 public double RotationAngle
210 {
211 get => (double)this.GetValue(RotationAngleProperty);
212 set => this.SetValue(RotationAngleProperty, value);
213 }
214
218 public static readonly BindableProperty LimitToBoundsProperty = BindableProperty.Create(
219 propertyName: nameof(LimitToBounds),
220 returnType: typeof(bool),
221 declaringType: typeof(ImageCropperView),
222 defaultValue: true);
223
227 public bool LimitToBounds
228 {
229 get => (bool)this.GetValue(LimitToBoundsProperty);
230 set => this.SetValue(LimitToBoundsProperty, value);
231 }
232
233 #endregion
234
235 #region Instance Fields
236
240 private float scale = 1f;
241
245 private float offsetX;
246
250 private float offsetY;
251
255 private float startOffsetX;
256
260 private float startOffsetY;
261
265 private Point pinchPivot = new Point(0.5, 0.5);
266
270 private SKBitmap? bitmap;
271
275 private readonly SKCanvasView canvasView;
276
280 private bool initialPositionSet;
281
282 #endregion
283
284 #region Constructor
285
290 {
291 // Create a root grid layout.
292 Grid Root = new Grid
293 {
294 VerticalOptions = LayoutOptions.Fill,
295 HorizontalOptions = LayoutOptions.Fill
296 };
297
298 // Initialize the canvas view.
299 this.canvasView = new SKCanvasView
300 {
301 VerticalOptions = LayoutOptions.Fill,
302 HorizontalOptions = LayoutOptions.Fill
303 };
304
305 // Set up pan and pinch gesture recognizers.
306 PanGestureRecognizer panGesture = new() { TouchPoints = 1 };
307 panGesture.PanUpdated += this.OnPan;
308
309 PinchGestureRecognizer pinchGesture = new();
310 pinchGesture.PinchUpdated += this.OnPinch;
311
312 this.canvasView.GestureRecognizers.Add(panGesture);
313 this.canvasView.GestureRecognizers.Add(pinchGesture);
314
315 // Subscribe to the canvas paint event.
316 this.canvasView.PaintSurface += this.OnPaintSurface; // No need to unregister as the canvasView has the same lifetime
317
318 Root.Add(this.canvasView);
319 this.Content = Root;
320 }
321 #endregion
322
323 #region Public Methods
324
329 {
330 this.scale = 1f;
331 this.offsetX = 0f;
332 this.offsetY = 0f;
333 this.startOffsetX = 0f;
334 this.startOffsetY = 0f;
335 this.canvasView.InvalidateSurface();
336 }
337
343 public byte[]? PerformCrop()
344 {
345 try
346 {
347 if (this.bitmap is null)
348 return null;
349
350 int viewWidth = (int)this.canvasView.CanvasSize.Width;
351 int viewHeight = (int)this.canvasView.CanvasSize.Height;
352 if (viewWidth <= 0 || viewHeight <= 0)
353 return null;
354
355 // Render the transformed image to a temporary surface.
356 using SKBitmap tempSurfaceBitmap = new SKBitmap(viewWidth, viewHeight, isOpaque: false);
357 using SKCanvas tempCanvas = new SKCanvas(tempSurfaceBitmap);
358 tempCanvas.Clear(SKColors.Transparent);
359
360 float centerX = viewWidth / 2f;
361 float centerY = viewHeight / 2f;
362
363 tempCanvas.Save();
364 tempCanvas.Translate(this.offsetX, this.offsetY);
365 tempCanvas.Scale(this.scale);
366 tempCanvas.Translate(centerX, centerY);
367 tempCanvas.RotateDegrees((float)this.RotationAngle);
368 tempCanvas.Translate(-centerX, -centerY);
369
370 SKRect destRect = new SKRect(0, 0, this.bitmap.Width, this.bitmap.Height);
371 tempCanvas.DrawBitmap(this.bitmap, destRect);
372 tempCanvas.Restore();
373
374 // Define the crop shape.
375 SKRect cropRect = this.ComputeCropShapeRect(viewWidth, viewHeight, this.CropShapeFillPortion);
376 SKPath cropPath = new SKPath();
377 switch (this.CropMode)
378 {
379 case CropMode.Circle:
380 {
381 float radius = cropRect.Width / 2f;
382 float cx = cropRect.MidX;
383 float cy = cropRect.MidY;
384 cropPath.AddCircle(cx, cy, radius);
385 break;
386 }
387 case CropMode.Square:
388 case CropMode.Aspect:
389 cropPath.AddRect(cropRect);
390 break;
391 }
392
393 // Apply the crop mask.
394 using SKBitmap maskedBitmap = new SKBitmap(viewWidth, viewHeight, isOpaque: false);
395 using (SKCanvas maskedCanvas = new SKCanvas(maskedBitmap))
396 {
397 maskedCanvas.Clear(SKColors.Transparent);
398 maskedCanvas.ClipPath(cropPath, SKClipOperation.Intersect, true);
399
400 maskedCanvas.Save();
401 maskedCanvas.Translate(this.offsetX, this.offsetY);
402 maskedCanvas.Scale(this.scale);
403 maskedCanvas.Translate(centerX, centerY);
404 maskedCanvas.RotateDegrees((float)this.RotationAngle);
405 maskedCanvas.Translate(-centerX, -centerY);
406 maskedCanvas.DrawBitmap(this.bitmap, destRect);
407 maskedCanvas.Restore();
408 }
409
410 // Extract the bounding box of the crop area.
411 int left = (int)Math.Floor(cropRect.Left);
412 int top = (int)Math.Floor(cropRect.Top);
413 int right = (int)Math.Ceiling(cropRect.Right);
414 int bottom = (int)Math.Ceiling(cropRect.Bottom);
415
416 SKRectI BoundingBox = new SKRectI(left, top, right, bottom);
417 using SKBitmap cropped = new SKBitmap(BoundingBox.Width, BoundingBox.Height);
418 using (SKCanvas croppedCanvas = new SKCanvas(cropped))
419 {
420 SKRect dst = new SKRect(0, 0, BoundingBox.Width, BoundingBox.Height);
421 croppedCanvas.DrawBitmap(maskedBitmap, BoundingBox, dst);
422 }
423
424 // Resize the cropped bitmap if needed.
425 SKBitmap finalBitmap = ResizeBitmapIfNeeded(cropped,
426 (int)this.OutputMaxResolution.Width,
427 (int)this.OutputMaxResolution.Height);
428
429 // Encode the final bitmap to the specified format.
430 using MemoryStream ms = new MemoryStream();
431 if (this.OutputFormat == CropOutputFormat.Png)
432 {
433 finalBitmap.Encode(ms, SKEncodedImageFormat.Png, 100);
434 }
435 else
436 {
437 int quality = Math.Max(0, Math.Min(100, this.JpegQuality));
438 finalBitmap.Encode(ms, SKEncodedImageFormat.Jpeg, quality);
439 }
440 return ms?.ToArray();
441 }
442 catch (Exception ex)
443 {
444 Console.WriteLine($"Error during cropping: {ex.Message}");
445 return null;
446 }
447 }
448
449 #endregion
450
451 #region Gesture Handlers
452
458 private void OnPan(object? sender, PanUpdatedEventArgs e)
459 {
460 double density = DeviceDisplay.MainDisplayInfo.Density;
461
462 switch (e.StatusType)
463 {
464 case GestureStatus.Started:
465 this.startOffsetX = this.offsetX;
466 this.startOffsetY = this.offsetY;
467 break;
468 case GestureStatus.Running:
469 this.offsetX = this.startOffsetX + (float)(e.TotalX * density);
470 this.offsetY = this.startOffsetY + (float)(e.TotalY * density);
471 if (this.LimitToBounds)
472 {
473 this.ClampTransformations();
474 }
475 this.canvasView.InvalidateSurface();
476 break;
477 }
478 }
479
485 private void OnPinch(object? sender, PinchGestureUpdatedEventArgs e)
486 {
487 switch (e.Status)
488 {
489 case GestureStatus.Started:
490 // Record the current offsets and the pinch pivot.
491 this.startOffsetX = this.offsetX;
492 this.startOffsetY = this.offsetY;
493 this.pinchPivot = e.ScaleOrigin;
494 break;
495 case GestureStatus.Running:
496 // Use the incremental scale factor directly.
497 float delta = (float)e.Scale; // e.Scale is the incremental factor.
498
499 // Update the cumulative scale.
500 this.scale *= delta;
501
502 // Convert the pinch pivot (relative coordinates) to actual pixels.
503 float pivotX = (float)(this.pinchPivot.X * this.canvasView.CanvasSize.Width);
504 float pivotY = (float)(this.pinchPivot.Y * this.canvasView.CanvasSize.Height);
505
506 // Adjust offsets to keep the pivot point stable.
507 this.offsetX = pivotX - (pivotX - this.offsetX) * delta;
508 this.offsetY = pivotY - (pivotY - this.offsetY) * delta;
509
510 if (this.LimitToBounds)
511 {
512 this.ClampTransformations();
513 }
514 this.canvasView.InvalidateSurface();
515 break;
516 case GestureStatus.Completed:
517 case GestureStatus.Canceled:
518 break;
519 }
520 }
521
522 #endregion
523
524 #region Paint & Layout
525
531 private void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
532 {
533 SKCanvas canvas = e.Surface.Canvas;
534 canvas.Clear(SKColors.Transparent);
535
536 if (this.bitmap is null)
537 return;
538
539 float centerX = e.Info.Width / 2f;
540 float centerY = e.Info.Height / 2f;
541
542 canvas.Save();
543 canvas.Translate(this.offsetX, this.offsetY);
544 canvas.Scale(this.scale);
545 canvas.Translate(centerX, centerY);
546 canvas.RotateDegrees((float)this.RotationAngle);
547 canvas.Translate(-centerX, -centerY);
548 SKRect destRect = new SKRect(0, 0, this.bitmap.Width, this.bitmap.Height);
549 canvas.DrawBitmap(this.bitmap, destRect);
550 canvas.Restore();
551
552 SKRect shapeRect = this.ComputeCropShapeRect(e.Info.Width, e.Info.Height, this.CropShapeFillPortion);
553 SKPath path = new SKPath();
554 switch (this.CropMode)
555 {
556 case CropMode.Circle:
557 {
558 float radius = shapeRect.Width / 2f;
559 float cx = shapeRect.MidX;
560 float cy = shapeRect.MidY;
561 path.AddCircle(cx, cy, radius);
562 break;
563 }
564 case CropMode.Square:
565 case CropMode.Aspect:
566 path.AddRect(shapeRect);
567 break;
568 }
569
570 using (SKPaint overlayPaint = new SKPaint
571 {
572 Color = new SKColor(0, 0, 0, 100),
573 Style = SKPaintStyle.Fill
574 })
575 {
576 canvas.Save();
577 canvas.ClipPath(path, SKClipOperation.Difference, true);
578 canvas.DrawRect(new SKRect(0, 0, e.Info.Width, e.Info.Height), overlayPaint);
579 canvas.Restore();
580 }
581
582 using (SKPaint outlinePaint = new SKPaint
583 {
584 Color = SKColors.White,
585 Style = SKPaintStyle.Stroke,
586 StrokeWidth = 3,
587 IsAntialias = true
588 })
589 {
590 canvas.DrawPath(path, outlinePaint);
591 }
592 }
593
599 protected override void OnSizeAllocated(double width, double height)
600 {
601 base.OnSizeAllocated(width, height);
602
603 if (!this.initialPositionSet && width > 0 && height > 0 && this.bitmap is not null)
604 {
605 this.SetInitialImagePosition(width, height);
606 this.initialPositionSet = true;
607 this.canvasView.InvalidateSurface();
608 }
609 }
610
616 private void SetInitialImagePosition(double width, double height)
617 {
618 double density = DeviceDisplay.MainDisplayInfo.Density;
619 float viewWidthPx = (float)(width * density);
620 float viewHeightPx = (float)(height * density);
621
622 SKRect cropShapeRect = this.ComputeCropShapeRect(viewWidthPx, viewHeightPx, this.CropShapeFillPortion);
623 float cropCenterX = cropShapeRect.MidX;
624 float cropCenterY = cropShapeRect.MidY;
625
626 float scaleX = cropShapeRect.Width / this.bitmap!.Width;
627 float scaleY = cropShapeRect.Height / this.bitmap.Height;
628 float newScale = MathF.Max(scaleX, scaleY);
629 this.scale = newScale;
630
631 float scaledWidth = this.bitmap.Width * newScale;
632 float scaledHeight = this.bitmap.Height * newScale;
633
634 this.offsetX = cropCenterX - (scaledWidth / 2f);
635 this.offsetY = cropCenterY - (scaledHeight / 2f);
636 }
637
645 private SKRect ComputeCropShapeRect(float containerWidth, float containerHeight, float marginFactor)
646 {
647 marginFactor = MathF.Max(0.0f, MathF.Min(1.0f, marginFactor));
648
649 switch (this.CropMode)
650 {
651 case CropMode.Circle:
652 case CropMode.Square:
653 {
654 float minDim = MathF.Min(containerWidth, containerHeight);
655 float shapeSize = minDim * marginFactor;
656 float left = (containerWidth - shapeSize) / 2f;
657 float top = (containerHeight - shapeSize) / 2f;
658 return new SKRect(left, top, left + shapeSize, top + shapeSize);
659 }
660 case CropMode.Aspect:
661 {
662 float targetRatio = 1.0f;
663 if (this.OutputMaxResolution.Height > 0)
664 {
665 targetRatio = (float)this.OutputMaxResolution.Width / (float)this.OutputMaxResolution.Height;
666 }
667 float shapeWidth = containerWidth * marginFactor;
668 float shapeHeight = shapeWidth / targetRatio;
669 if (shapeHeight > containerHeight * marginFactor)
670 {
671 shapeHeight = containerHeight * marginFactor;
672 shapeWidth = shapeHeight * targetRatio;
673 }
674 float left = (containerWidth - shapeWidth) / 2f;
675 float top = (containerHeight - shapeHeight) / 2f;
676 return new SKRect(left, top, left + shapeWidth, top + shapeHeight);
677 }
678 }
679 return SKRect.Empty;
680 }
681
682 #endregion
683
684 #region Bitmap Loading & Resizing
685
692 private static void OnRotationAngleChanged(BindableObject bindable, object oldValue, object newValue)
693 {
694 if (bindable is ImageCropperView cropper)
695 {
696 cropper.ClampTransformations();
697 cropper.canvasView.InvalidateSurface();
698 }
699 }
700
708 private static async void OnImageSourceChanged(BindableObject bindable, object oldValue, object newValue)
709 {
710 if (bindable is ImageCropperView cropper && newValue is ImageSource newSource)
711 {
712 cropper.bitmap?.Dispose();
713 cropper.bitmap = await LoadAndResizeBitmapAsync(newSource, cropper.InputMaxResolution);
714 // Optionally, reset transformations:
715 // cropper.ResetTransformations();
716
717 cropper.initialPositionSet = false;
718
719 if (cropper.Width > 0 && cropper.Height > 0)
720 {
721 cropper.SetInitialImagePosition(cropper.Width, cropper.Height);
722 cropper.initialPositionSet = true;
723 }
724 else
725 {
726 cropper.initialPositionSet = false;
727 }
728
729 cropper.canvasView.InvalidateSurface();
730 }
731 }
732
739 private static async Task<SKBitmap?> LoadAndResizeBitmapAsync(ImageSource imageSource, Size maxResolution)
740 {
741 try
742 {
743 StreamImageSource? streamImageSource = null;
744 switch (imageSource)
745 {
746 case FileImageSource fileSource:
747 if (File.Exists(fileSource.File))
748 {
749 streamImageSource = new StreamImageSource
750 {
751 Stream = _ => Task.FromResult<Stream>(File.OpenRead(fileSource.File))
752 };
753 }
754 break;
755 case UriImageSource uriSource:
756 {
757 HttpResponseMessage response = await sHttpClient.GetAsync(uriSource.Uri);
758 if (response.IsSuccessStatusCode)
759 {
760 MemoryStream ms = new MemoryStream();
761 await response.Content.CopyToAsync(ms);
762 ms.Position = 0;
763 streamImageSource = new StreamImageSource
764 {
765 Stream = _ => Task.FromResult<Stream>(ms)
766 };
767 }
768 break;
769 }
770 case StreamImageSource sis:
771 streamImageSource = sis;
772 break;
773 }
774
775 if (streamImageSource is null)
776 return null;
777
778 Stream stream = await streamImageSource.Stream(CancellationToken.None).ConfigureAwait(false);
779 if (stream is null)
780 return null;
781
782 using MemoryStream managedStream = new MemoryStream();
783 await stream.CopyToAsync(managedStream).ConfigureAwait(false);
784 managedStream.Position = 0;
785
786 SKBitmap loadedBitmap = SKBitmap.Decode(managedStream);
787 if (loadedBitmap is null)
788 return null;
789
790 SKBitmap resized = ResizeBitmapIfNeeded(loadedBitmap,
791 (int)maxResolution.Width,
792 (int)maxResolution.Height);
793
794 if (resized != loadedBitmap)
795 {
796 loadedBitmap.Dispose();
797 }
798 return resized;
799 }
800 catch (Exception ex)
801 {
802 Console.WriteLine($"Error loading bitmap: {ex.Message}");
803 return null;
804 }
805 }
806
816 private static SKBitmap ResizeBitmapIfNeeded(SKBitmap sourceBitmap, int maxWidth, int maxHeight)
817 {
818 if (sourceBitmap is null || maxWidth <= 0 || maxHeight <= 0)
819 return new SKBitmap(1, 1);
820
821 int width = sourceBitmap.Width;
822 int height = sourceBitmap.Height;
823
824 if (width <= maxWidth && height <= maxHeight)
825 {
826 return sourceBitmap;
827 }
828
829 float widthRatio = (float)maxWidth / width;
830 float heightRatio = (float)maxHeight / height;
831 float scale = Math.Min(widthRatio, heightRatio);
832
833 int newWidth = (int)(width * scale);
834 int newHeight = (int)(height * scale);
835
836 SKBitmap resized = new SKBitmap(newWidth, newHeight, sourceBitmap.ColorType, sourceBitmap.AlphaType);
837 using SKCanvas canvas = new SKCanvas(resized);
838 using SKPaint paint = new SKPaint
839 {
840 FilterQuality = SKFilterQuality.High,
841 IsAntialias = true
842 };
843
844 SKRect srcRect = new SKRect(0, 0, width, height);
845 SKRect destRect = new SKRect(0, 0, newWidth, newHeight);
846 canvas.DrawBitmap(sourceBitmap, srcRect, destRect, paint);
847
848 return resized;
849 }
850
851 #endregion
852
857 private void ClampTransformations()
858 {
859 if (!this.LimitToBounds || this.bitmap is null)
860 return;
861
862 SKSize canvasSize = this.canvasView.CanvasSize;
863 if (canvasSize.Width <= 0 || canvasSize.Height <= 0)
864 return;
865
866 float canvasWidth = canvasSize.Width;
867 float canvasHeight = canvasSize.Height;
868 // Compute the crop rectangle (in canvas coordinates).
869 SKRect cropRect = this.ComputeCropShapeRect(canvasWidth, canvasHeight, this.CropShapeFillPortion);
870
871 // -----------------------------------------------------
872 // 1. Enforce a Minimum Scale (Zoom)
873 // -----------------------------------------------------
874 // When the image is rotated, its axis-aligned bounding box is larger than the original.
875 // Compute the bounding box dimensions of the image after rotation (at scale = 1).
876 float angle = (float)this.RotationAngle;
877 float rad = angle * (MathF.PI / 180f);
878 float absCos = MathF.Abs(MathF.Cos(rad));
879 float absSin = MathF.Abs(MathF.Sin(rad));
880
881 float rotatedWidth = this.bitmap.Width * absCos + this.bitmap.Height * absSin;
882 float rotatedHeight = this.bitmap.Width * absSin + this.bitmap.Height * absCos;
883
884 // Compute the minimal scale needed so that the rotated image’s bounding box covers the cropRect.
885 float minScaleX = cropRect.Width / rotatedWidth;
886 float minScaleY = cropRect.Height / rotatedHeight;
887 float minScale = MathF.Max(minScaleX, minScaleY);
888
889 // Enforce the minimum scale.
890 if (this.scale < minScale)
891 {
892 this.scale = minScale;
893 }
894
895 // -----------------------------------------------------
896 // 2. Adjust the Offset (Translation)
897 // -----------------------------------------------------
898 // In OnPaintSurface the image is drawn with:
899 // canvas.Translate(offsetX, offsetY);
900 // canvas.Scale(scale);
901 // canvas.Translate(centerX, centerY);
902 // canvas.RotateDegrees(RotationAngle);
903 // canvas.Translate(-centerX, -centerY);
904 //
905 // This is equivalent to transforming any image point p as:
906 // p' = offset + scale * (center + R*(p - center))
907 // where the canvas center is:
908 SKPoint Center = new SKPoint(canvasWidth / 2, canvasHeight / 2);
909
910 // Compute the transformed (but not offset-adjusted) positions of the image corners.
911 float LocalMinX = float.MaxValue;
912 float LocalMaxX = float.MinValue;
913 float LocalMinY = float.MaxValue;
914 float LocalMaxY = float.MinValue;
915
916 // The four corners of the original image.
917 SKPoint[] Corners =
918 [
919 new SKPoint(0, 0),
920 new SKPoint(this.bitmap.Width, 0),
921 new SKPoint(this.bitmap.Width, this.bitmap.Height),
922 new SKPoint(0, this.bitmap.Height)
923 ];
924
925 float cos = MathF.Cos(rad);
926 float sin = MathF.Sin(rad);
927
928 foreach (SKPoint p in Corners)
929 {
930 // Compute the vector from the canvas center.
931 SKPoint Diff = new SKPoint(p.X - Center.X, p.Y - Center.Y);
932 // Rotate the vector.
933 SKPoint Rotated = new SKPoint(Diff.X * cos - Diff.Y * sin,
934 Diff.X * sin + Diff.Y * cos);
935 // Apply scaling and add back the canvas center.
936 SKPoint TransformedLocal = new SKPoint(
937 this.scale * (Center.X + Rotated.X),
938 this.scale * (Center.Y + Rotated.Y)
939 );
940
941 LocalMinX = MathF.Min(LocalMinX, TransformedLocal.X);
942 LocalMaxX = MathF.Max(LocalMaxX, TransformedLocal.X);
943 LocalMinY = MathF.Min(LocalMinY, TransformedLocal.Y);
944 LocalMaxY = MathF.Max(LocalMaxY, TransformedLocal.Y);
945 }
946
947 // The final (drawn) image bounds are the local bounds shifted by the offset.
948 float ImageMinX = this.offsetX + LocalMinX;
949 float ImageMaxX = this.offsetX + LocalMaxX;
950 float ImageMinY = this.offsetY + LocalMinY;
951 float ImageMaxY = this.offsetY + LocalMaxY;
952
953 // Determine the adjustments needed so that the image bounds fully cover the crop area.
954 float DeltaX = 0;
955 float DeltaY = 0;
956
957 // If the image is too far right (its left edge is right of the crop's left), shift left.
958 if (ImageMinX > cropRect.Left)
959 {
960 DeltaX = cropRect.Left - ImageMinX;
961 }
962 // Or if the image is too far left (its right edge is left of the crop's right), shift right.
963 else if (ImageMaxX < cropRect.Right)
964 {
965 DeltaX = cropRect.Right - ImageMaxX;
966 }
967
968 // Do the same for vertical adjustment.
969 if (ImageMinY > cropRect.Top)
970 {
971 DeltaY = cropRect.Top - ImageMinY;
972 }
973 else if (ImageMaxY < cropRect.Bottom)
974 {
975 DeltaY = cropRect.Bottom - ImageMaxY;
976 }
977
978 this.offsetX += DeltaX;
979 this.offsetY += DeltaY;
980 }
981
982
983 }
984}
A custom view that displays an image with pinch-zoom, pan, and rotation support, and allows cropping ...
ImageCropperView()
Initializes a new instance of the ImageCropperView class.
bool LimitToBounds
When true, prevents moving or zooming the image so that it does not fully cover the crop area.
ImageSource ImageSource
Gets or sets the image source to display and crop.
static readonly BindableProperty JpegQualityProperty
Backing store for JpegQuality.
static readonly BindableProperty OutputMaxResolutionProperty
Backing store for OutputMaxResolution.
static readonly BindableProperty RotationAngleProperty
Backing store for RotationAngle.
CropMode CropMode
Gets or sets the crop mode.
CropOutputFormat OutputFormat
Gets or sets the output image format.
double RotationAngle
Gets or sets the rotation angle (in degrees) for the image.
static readonly BindableProperty CropShapeFillPortionProperty
Backing store for CropShapeFillPortion.
byte?[] PerformCrop()
Performs cropping of the image using the current pan, zoom, and rotation transformations....
static readonly BindableProperty LimitToBoundsProperty
Backing store for LimitToBounds.
float CropShapeFillPortion
Gets or sets the fraction of the view filled by the crop shape.
static readonly BindableProperty OutputFormatProperty
Backing store for OutputFormat.
static readonly BindableProperty ImageSourceProperty
Backing store for ImageSource.
static readonly BindableProperty CropModeProperty
Backing store for CropMode.
override void OnSizeAllocated(double width, double height)
Overrides the OnSizeAllocated method to set the initial image position when the view is first laid ou...
Size OutputMaxResolution
Gets or sets the maximum resolution for the cropped output image.
int JpegQuality
Gets or sets the JPEG quality when encoding as JPEG.
static readonly BindableProperty InputMaxResolutionProperty
Backing store for InputMaxResolution.
void ResetTransformations()
Resets the image transformations (pan, zoom, rotation) to their default values.
Size InputMaxResolution
Gets or sets the maximum resolution for the input image.
Definition: ImplTypes.g.cs:58
CropMode
Specifies how the image should be cropped.
CropOutputFormat
Specifies the output format of the cropped image.