5using System.Threading.Tasks;
6using Microsoft.Maui.Controls;
8using SkiaSharp.Views.Maui;
9using SkiaSharp.Views.Maui.Controls;
61#warning Replace this with InternetContent
65 private static readonly HttpClient sHttpClient =
new HttpClient();
67 #region Bindable Properties
74 returnType: typeof(Size),
76 defaultValue:
new Size(1280, 1280));
92 returnType: typeof(Size),
94 defaultValue:
new Size(3840, 2160));
109 propertyName: nameof(ImageSource),
110 returnType: typeof(ImageSource),
113 propertyChanged: OnImageSourceChanged);
118 public ImageSource ImageSource
165 returnType: typeof(
int),
183 returnType: typeof(
float),
201 returnType: typeof(
double),
204 propertyChanged: OnRotationAngleChanged);
220 returnType: typeof(
bool),
235 #region Instance Fields
240 private float scale = 1f;
245 private float offsetX;
250 private float offsetY;
255 private float startOffsetX;
260 private float startOffsetY;
265 private Point pinchPivot =
new Point(0.5, 0.5);
270 private SKBitmap? bitmap;
275 private readonly SKCanvasView canvasView;
280 private bool initialPositionSet;
294 VerticalOptions = LayoutOptions.Fill,
295 HorizontalOptions = LayoutOptions.Fill
299 this.canvasView =
new SKCanvasView
301 VerticalOptions = LayoutOptions.Fill,
302 HorizontalOptions = LayoutOptions.Fill
306 PanGestureRecognizer panGesture =
new() { TouchPoints = 1 };
307 panGesture.PanUpdated += this.OnPan;
309 PinchGestureRecognizer pinchGesture =
new();
310 pinchGesture.PinchUpdated += this.OnPinch;
312 this.canvasView.GestureRecognizers.Add(panGesture);
313 this.canvasView.GestureRecognizers.Add(pinchGesture);
316 this.canvasView.PaintSurface += this.OnPaintSurface;
318 Root.Add(this.canvasView);
323 #region Public Methods
333 this.startOffsetX = 0f;
334 this.startOffsetY = 0f;
335 this.canvasView.InvalidateSurface();
347 if (this.bitmap is
null)
350 int viewWidth = (int)this.canvasView.CanvasSize.Width;
351 int viewHeight = (
int)this.canvasView.CanvasSize.Height;
352 if (viewWidth <= 0 || viewHeight <= 0)
356 using SKBitmap tempSurfaceBitmap =
new SKBitmap(viewWidth, viewHeight, isOpaque:
false);
357 using SKCanvas tempCanvas =
new SKCanvas(tempSurfaceBitmap);
358 tempCanvas.Clear(SKColors.Transparent);
360 float centerX = viewWidth / 2f;
361 float centerY = viewHeight / 2f;
364 tempCanvas.Translate(this.offsetX, this.offsetY);
365 tempCanvas.Scale(this.scale);
366 tempCanvas.Translate(centerX, centerY);
368 tempCanvas.Translate(-centerX, -centerY);
370 SKRect destRect =
new SKRect(0, 0, this.bitmap.Width,
this.bitmap.Height);
371 tempCanvas.DrawBitmap(this.bitmap, destRect);
372 tempCanvas.Restore();
376 SKPath cropPath =
new SKPath();
381 float radius = cropRect.Width / 2f;
382 float cx = cropRect.MidX;
383 float cy = cropRect.MidY;
384 cropPath.AddCircle(cx, cy, radius);
389 cropPath.AddRect(cropRect);
394 using SKBitmap maskedBitmap =
new SKBitmap(viewWidth, viewHeight, isOpaque:
false);
395 using (SKCanvas maskedCanvas =
new SKCanvas(maskedBitmap))
397 maskedCanvas.Clear(SKColors.Transparent);
398 maskedCanvas.ClipPath(cropPath, SKClipOperation.Intersect,
true);
401 maskedCanvas.Translate(this.offsetX, this.offsetY);
402 maskedCanvas.Scale(this.scale);
403 maskedCanvas.Translate(centerX, centerY);
405 maskedCanvas.Translate(-centerX, -centerY);
406 maskedCanvas.DrawBitmap(this.bitmap, destRect);
407 maskedCanvas.Restore();
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);
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))
420 SKRect dst =
new SKRect(0, 0, BoundingBox.Width, BoundingBox.Height);
421 croppedCanvas.DrawBitmap(maskedBitmap, BoundingBox, dst);
425 SKBitmap finalBitmap = ResizeBitmapIfNeeded(cropped,
427 (
int)
this.OutputMaxResolution.Height);
430 using MemoryStream ms =
new MemoryStream();
433 finalBitmap.Encode(ms, SKEncodedImageFormat.Png, 100);
437 int quality = Math.Max(0, Math.Min(100,
this.JpegQuality));
438 finalBitmap.Encode(ms, SKEncodedImageFormat.Jpeg, quality);
440 return ms?.ToArray();
444 Console.WriteLine($
"Error during cropping: {ex.Message}");
451 #region Gesture Handlers
458 private void OnPan(
object? sender, PanUpdatedEventArgs e)
460 double density = DeviceDisplay.MainDisplayInfo.Density;
462 switch (e.StatusType)
464 case GestureStatus.Started:
465 this.startOffsetX = this.offsetX;
466 this.startOffsetY = this.offsetY;
468 case GestureStatus.Running:
469 this.offsetX = this.startOffsetX + (float)(e.TotalX * density);
470 this.offsetY = this.startOffsetY + (float)(e.TotalY * density);
473 this.ClampTransformations();
475 this.canvasView.InvalidateSurface();
485 private void OnPinch(
object? sender, PinchGestureUpdatedEventArgs e)
489 case GestureStatus.Started:
491 this.startOffsetX = this.offsetX;
492 this.startOffsetY = this.offsetY;
493 this.pinchPivot = e.ScaleOrigin;
495 case GestureStatus.Running:
497 float delta = (float)e.Scale;
503 float pivotX = (
float)(this.pinchPivot.X * this.canvasView.CanvasSize.Width);
504 float pivotY = (float)(this.pinchPivot.Y *
this.canvasView.CanvasSize.Height);
507 this.offsetX = pivotX - (pivotX - this.offsetX) * delta;
508 this.offsetY = pivotY - (pivotY - this.offsetY) * delta;
512 this.ClampTransformations();
514 this.canvasView.InvalidateSurface();
516 case GestureStatus.Completed:
517 case GestureStatus.Canceled:
524 #region Paint & Layout
531 private void OnPaintSurface(
object? sender, SKPaintSurfaceEventArgs e)
533 SKCanvas canvas = e.Surface.Canvas;
534 canvas.Clear(SKColors.Transparent);
536 if (this.bitmap is
null)
539 float centerX = e.Info.Width / 2f;
540 float centerY = e.Info.Height / 2f;
543 canvas.Translate(this.offsetX, this.offsetY);
544 canvas.Scale(this.scale);
545 canvas.Translate(centerX, centerY);
547 canvas.Translate(-centerX, -centerY);
548 SKRect destRect =
new SKRect(0, 0, this.bitmap.Width,
this.bitmap.Height);
549 canvas.DrawBitmap(this.bitmap, destRect);
552 SKRect shapeRect = this.ComputeCropShapeRect(e.Info.Width, e.Info.Height,
this.CropShapeFillPortion);
553 SKPath path =
new SKPath();
558 float radius = shapeRect.Width / 2f;
559 float cx = shapeRect.MidX;
560 float cy = shapeRect.MidY;
561 path.AddCircle(cx, cy, radius);
566 path.AddRect(shapeRect);
570 using (SKPaint overlayPaint =
new SKPaint
572 Color =
new SKColor(0, 0, 0, 100),
573 Style = SKPaintStyle.Fill
577 canvas.ClipPath(path, SKClipOperation.Difference,
true);
578 canvas.DrawRect(
new SKRect(0, 0, e.Info.Width, e.Info.Height), overlayPaint);
582 using (SKPaint outlinePaint =
new SKPaint
584 Color = SKColors.White,
585 Style = SKPaintStyle.Stroke,
590 canvas.DrawPath(path, outlinePaint);
601 base.OnSizeAllocated(width, height);
603 if (!this.initialPositionSet && width > 0 && height > 0 && this.bitmap is not
null)
605 this.SetInitialImagePosition(width, height);
606 this.initialPositionSet =
true;
607 this.canvasView.InvalidateSurface();
616 private void SetInitialImagePosition(
double width,
double height)
618 double density = DeviceDisplay.MainDisplayInfo.Density;
619 float viewWidthPx = (float)(width * density);
620 float viewHeightPx = (float)(height * density);
622 SKRect cropShapeRect = this.ComputeCropShapeRect(viewWidthPx, viewHeightPx, this.
CropShapeFillPortion);
623 float cropCenterX = cropShapeRect.MidX;
624 float cropCenterY = cropShapeRect.MidY;
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;
631 float scaledWidth = this.bitmap.Width * newScale;
632 float scaledHeight = this.bitmap.Height * newScale;
634 this.offsetX = cropCenterX - (scaledWidth / 2f);
635 this.offsetY = cropCenterY - (scaledHeight / 2f);
645 private SKRect ComputeCropShapeRect(
float containerWidth,
float containerHeight,
float marginFactor)
647 marginFactor = MathF.Max(0.0f, MathF.Min(1.0f, marginFactor));
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);
662 float targetRatio = 1.0f;
667 float shapeWidth = containerWidth * marginFactor;
668 float shapeHeight = shapeWidth / targetRatio;
669 if (shapeHeight > containerHeight * marginFactor)
671 shapeHeight = containerHeight * marginFactor;
672 shapeWidth = shapeHeight * targetRatio;
674 float left = (containerWidth - shapeWidth) / 2f;
675 float top = (containerHeight - shapeHeight) / 2f;
676 return new SKRect(left, top, left + shapeWidth, top + shapeHeight);
684 #region Bitmap Loading & Resizing
692 private static void OnRotationAngleChanged(BindableObject bindable,
object oldValue,
object newValue)
696 cropper.ClampTransformations();
697 cropper.canvasView.InvalidateSurface();
708 private static async
void OnImageSourceChanged(BindableObject bindable,
object oldValue,
object newValue)
710 if (bindable is
ImageCropperView cropper && newValue is ImageSource newSource)
712 cropper.bitmap?.Dispose();
713 cropper.bitmap = await LoadAndResizeBitmapAsync(newSource, cropper.InputMaxResolution);
717 cropper.initialPositionSet =
false;
719 if (cropper.Width > 0 && cropper.Height > 0)
721 cropper.SetInitialImagePosition(cropper.Width, cropper.Height);
722 cropper.initialPositionSet =
true;
726 cropper.initialPositionSet =
false;
729 cropper.canvasView.InvalidateSurface();
739 private static async Task<SKBitmap?> LoadAndResizeBitmapAsync(ImageSource imageSource, Size maxResolution)
743 StreamImageSource? streamImageSource =
null;
746 case FileImageSource fileSource:
747 if (File.Exists(fileSource.File))
749 streamImageSource =
new StreamImageSource
751 Stream =
_ => Task.FromResult<Stream>(File.OpenRead(fileSource.File))
755 case UriImageSource uriSource:
757 HttpResponseMessage response = await sHttpClient.GetAsync(uriSource.Uri);
758 if (response.IsSuccessStatusCode)
760 MemoryStream ms =
new MemoryStream();
761 await response.Content.CopyToAsync(ms);
763 streamImageSource =
new StreamImageSource
765 Stream =
_ => Task.FromResult<Stream>(ms)
770 case StreamImageSource sis:
771 streamImageSource = sis;
775 if (streamImageSource is
null)
778 Stream stream = await streamImageSource.Stream(CancellationToken.None).ConfigureAwait(
false);
782 using MemoryStream managedStream =
new MemoryStream();
783 await stream.CopyToAsync(managedStream).ConfigureAwait(
false);
784 managedStream.Position = 0;
786 SKBitmap loadedBitmap = SKBitmap.Decode(managedStream);
787 if (loadedBitmap is
null)
790 SKBitmap resized = ResizeBitmapIfNeeded(loadedBitmap,
791 (
int)maxResolution.Width,
792 (
int)maxResolution.Height);
794 if (resized != loadedBitmap)
796 loadedBitmap.Dispose();
802 Console.WriteLine($
"Error loading bitmap: {ex.Message}");
816 private static SKBitmap ResizeBitmapIfNeeded(SKBitmap sourceBitmap,
int maxWidth,
int maxHeight)
818 if (sourceBitmap is
null || maxWidth <= 0 || maxHeight <= 0)
819 return new SKBitmap(1, 1);
821 int width = sourceBitmap.Width;
822 int height = sourceBitmap.Height;
824 if (width <= maxWidth && height <= maxHeight)
829 float widthRatio = (float)maxWidth / width;
830 float heightRatio = (float)maxHeight / height;
831 float scale = Math.Min(widthRatio, heightRatio);
833 int newWidth = (int)(width * scale);
834 int newHeight = (int)(height * scale);
836 SKBitmap resized =
new SKBitmap(newWidth, newHeight, sourceBitmap.ColorType, sourceBitmap.AlphaType);
837 using SKCanvas canvas =
new SKCanvas(resized);
838 using SKPaint paint =
new SKPaint
840 FilterQuality = SKFilterQuality.High,
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);
857 private void ClampTransformations()
862 SKSize canvasSize = this.canvasView.CanvasSize;
863 if (canvasSize.Width <= 0 || canvasSize.Height <= 0)
866 float canvasWidth = canvasSize.Width;
867 float canvasHeight = canvasSize.Height;
869 SKRect cropRect = this.ComputeCropShapeRect(canvasWidth, canvasHeight, this.
CropShapeFillPortion);
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));
881 float rotatedWidth = this.bitmap.Width * absCos + this.bitmap.Height * absSin;
882 float rotatedHeight = this.bitmap.Width * absSin + this.bitmap.Height * absCos;
885 float minScaleX = cropRect.Width / rotatedWidth;
886 float minScaleY = cropRect.Height / rotatedHeight;
887 float minScale = MathF.Max(minScaleX, minScaleY);
890 if (this.scale < minScale)
892 this.scale = minScale;
908 SKPoint Center =
new SKPoint(canvasWidth / 2, canvasHeight / 2);
911 float LocalMinX =
float.MaxValue;
912 float LocalMaxX =
float.MinValue;
913 float LocalMinY =
float.MaxValue;
914 float LocalMaxY =
float.MinValue;
920 new SKPoint(this.bitmap.Width, 0),
921 new SKPoint(this.bitmap.Width,
this.bitmap.Height),
922 new SKPoint(0, this.bitmap.Height)
925 float cos = MathF.Cos(rad);
926 float sin = MathF.Sin(rad);
928 foreach (SKPoint p
in Corners)
931 SKPoint Diff =
new SKPoint(p.X - Center.X, p.Y - Center.Y);
933 SKPoint Rotated =
new SKPoint(Diff.X * cos - Diff.Y * sin,
934 Diff.X * sin + Diff.Y * cos);
936 SKPoint TransformedLocal =
new SKPoint(
937 this.scale * (Center.X + Rotated.X),
938 this.scale * (Center.Y + Rotated.Y)
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);
948 float ImageMinX = this.offsetX + LocalMinX;
949 float ImageMaxX = this.offsetX + LocalMaxX;
950 float ImageMinY = this.offsetY + LocalMinY;
951 float ImageMaxY = this.offsetY + LocalMaxY;
958 if (ImageMinX > cropRect.Left)
960 DeltaX = cropRect.Left - ImageMinX;
963 else if (ImageMaxX < cropRect.Right)
965 DeltaX = cropRect.Right - ImageMaxX;
969 if (ImageMinY > cropRect.Top)
971 DeltaY = cropRect.Top - ImageMinY;
973 else if (ImageMaxY < cropRect.Bottom)
975 DeltaY = cropRect.Bottom - ImageMaxY;
978 this.offsetX += DeltaX;
979 this.offsetY += DeltaY;
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.
CropMode
Specifies how the image should be cropped.
CropOutputFormat
Specifies the output format of the cropped image.