Crop image functionality doesn't work if Image is Square in Xamarin.Forms

Hi,

I have applied CropPhotos functionality in My Xamarin.Forms project. Everything is working fine. Only case is when I select any square image, it stuck and cropcontrol doesn't move.

Can anybody please suggest me what I am missing?

Here is full code

CropPhotoPage.xaml

<local:CropPhotoView Grid.Row="0" x:Name="CropPhotoView" OriginalImage="{Binding OriginalImage}" 
        CropTopLeftX="{Binding CropTopLeftX}" CropTopLeftY="{Binding CropTopLeftY}"
        CropWidth="{Binding CropWidth}" CropHeight="{Binding CropHeight}" />
<Button Text="Reset" Clicked="CancelClicked"  Command="{Binding CancelCommand}"  />

<Button Grid.Row="1" Clicked="ContinueClicked" Text="Continue" FontFamily="SourceSansPro-Regular" FontSize="17"
          Command="{Binding ContinueCommand}" />

CropPhotoPage.xaml.cs

public CropPhotoPage(CropPhotoViewModel bindingContext)
{
    InitializeComponent();
    BindingContext = bindingContext;
}

private void CancelClicked(object sender, EventArgs e)
{
    CropPhotoView.ResetCrop();
}

private void ContinueClicked(object sender, EventArgs e)
{
    CropPhotoView.ApplyCrop();
}

CropPhotoView.cs

public class CropPhotoView : View
{
    public static readonly BindableProperty CropTopLeftXProperty =
        BindableProperty.Create(nameof(CropTopLeftX), typeof (float), typeof (CropPhotoView), 0f, BindingMode.TwoWay);

    public static readonly BindableProperty CropTopLeftYProperty =
        BindableProperty.Create(nameof(CropTopLeftY), typeof (float), typeof (CropPhotoView), 0f, BindingMode.TwoWay);

    public static readonly BindableProperty CropWidthProperty =
        BindableProperty.Create(nameof(CropWidth), typeof (float), typeof (CropPhotoView), 0f, BindingMode.TwoWay);

    public static readonly BindableProperty CropHeightProperty =
        BindableProperty.Create(nameof(CropHeight), typeof (float), typeof (CropPhotoView), 0f, BindingMode.TwoWay);

    public static readonly BindableProperty OriginalImageProperty =
        BindableProperty.Create(nameof(OriginalImage), typeof(Image), typeof(CropPhotoView), null);

    public float CropTopLeftX
    {
        get { return (float) GetValue(CropTopLeftXProperty); }
        set { SetValue(CropTopLeftXProperty, value); }
    }

    public float CropTopLeftY
    {
        get { return (float) GetValue(CropTopLeftYProperty); }
        set { SetValue(CropTopLeftYProperty, value); }
    }

    public float CropWidth
    {
        get { return (float) GetValue(CropWidthProperty); }
        set { SetValue(CropWidthProperty, value); }
    }

    public float CropHeight
    {
        get { return (float) GetValue(CropHeightProperty); }
        set { SetValue(CropHeightProperty, value); }
    }

    public Image OriginalImage
    {
        get { return (Image)GetValue(OriginalImageProperty); }
        set { SetValue(OriginalImageProperty, value); }
    }

    public event Action ResetCropRequested;
    public event Action ApplyCropRequested;

    public void ResetCrop()
    {
        ResetCropRequested?.Invoke();
    }

    public void ApplyCrop()
    {
        ApplyCropRequested?.Invoke();
    }
}

CropViewModel

public CropPhotoViewModel(INavigationService notificationService,
    ICropImageService cropImageService,
    Image image)
    : this(notificationService, cropImageService)
{
    OriginalImage = image;
}

private bool IsLastImage { get; set; }

private Image originalImage;
public Image OriginalImage
{
    get => originalImage;
    set
    {
        originalImage = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OriginalImage))); //Notify the property is changing.
    }
}
public Image CroppedImage { get; set; }
public RelayCommand ContinueCommand { get; set; }
public RelayCommand CancelCommand { get; set; }

public double CropTopLeftX { get; set; }
public double CropTopLeftY { get; set; }
public double CropWidth { get; set; }
public double CropHeight { get; set; }

public event Action ImageCropped;
public event Action ImageCropCanceled;

private async Task ContinueCommandExecute()
{
    var croppedImage = _cropImageService.CropImageWithRect(OriginalImage,
        new Rectangle(CropTopLeftX, CropTopLeftY, CropWidth, CropHeight));
    CroppedImage = croppedImage;
    ImageCropped?.Invoke();
    this.Cleanup();
    _navigationService.GoBack();
}
private async Task CancelCommandExecute()
{
    ImageCropCanceled?.Invoke();
    _navigationService.GoBack();
}

iOS
CropImageService.cs

public class CropImageService : ICropImageService
{
    public Image CropImageWithRect (Image originalImage, Rectangle rectangle)
    {
        try
        {
            using (var image = ImageSourceToUIImageConverter.GetImageFromImageSource(originalImage.Source).Result)
            {
                try
                {
                    if (image == null)
                    {
                        return originalImage;
                    }

                    using (CGImage cr = image.CGImage.WithImageInRect(new CGRect(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)))
                    {
                        UIImage cropped = UIImage.FromImage(cr);
                        var croppedXF = new Image();
                        croppedXF.Source = ImageSource.FromStream(() => cropped.AsPNG().AsStream());
                        return croppedXF;
                    }
                }
                catch (System.Exception ex1)
                {
                    return null;
                }

            }
        }
        catch (System.Exception ex)
        {
            return null;
        }

    }
}

iOS
CropPhotoViewRenderer.cs

[assembly: ExportRenderer (typeof (CropPhotoView), typeof (CropPhotoViewRenderer))]
namespace Project.iOS
{
    public class CropPhotoViewRenderer : ViewRenderer
    {
        UIImageView imageView;
        CropperView cropperView;
        UIPanGestureRecognizer pan;
        UIPinchGestureRecognizer pinch;
        UITapGestureRecognizer doubleTap;

        protected override void OnElementChanged (ElementChangedEventArgs<View> e)
        {
            base.OnElementChanged (e);

            if (e.NewElement != null) 
            {
                e.NewElement.SizeChanged += async (sender, ea) => await NewElementOnSizeChanged(sender, ea);
                (e.NewElement as CropPhotoView).ResetCropRequested += () => 
                {
                    var imageRect = imageView.ConvertRectFromImage (new CGRect (0, 0, imageView.Image.Size.Width, imageView.Image.Size.Height));
                    var size = Math.Min (imageRect.Width, imageRect.Height);
                    cropperView.CropSize = new CGSize (size, size);
                    cropperView.Origin = new CGPoint ((Element.Width - cropperView.CropSize.Width) / 2, (Element.Height - cropperView.CropSize.Height) / 2);
                    cropperView.Frame = new CGRect (0, 0, Element.Width, Element.Height);
                };
            }
        }

        private async Task NewElementOnSizeChanged (object sender, EventArgs e)
        {
            try
            {
                var cropView = (Element as CropPhotoView).OriginalImage;

                using (var image = await ImageSourceToUIImageConverter.GetImageFromImageSource(cropView.Source))
                {
                    if (image.Size.Width != image.Size.Height)
                        imageView = new UIImageView(new CGRect(0, 0, Element.Width, Element.Height));
                    else
                        imageView = new UIImageView(new CGRect(0, (Element.Height - Element.Width) / 2, Element.Width, Element.Width));

                    imageView.Image = image;
                    imageView.ContentMode = UIViewContentMode.ScaleAspectFit;
                }
                var imageRect = imageView.ConvertRectFromImage(new CGRect(0, 0, imageView.Image.Size.Width, imageView.Image.Size.Height));
                var size = Math.Min(imageRect.Width, imageRect.Height);

                cropperView = new CropperView();
                cropperView.CropSize = new CGSize(size, size);
                cropperView.Origin = new CGPoint((Element.Width - cropperView.CropSize.Width) / 2, (Element.Height - cropperView.CropSize.Height) / 2);
                cropperView.Frame = new CGRect(0, 0, Element.Width, Element.Height);

                NativeView.AddSubviews(imageView, cropperView);

                nfloat dx = 0;
                nfloat dy = 0;

                pan = new UIPanGestureRecognizer(() => {
                    if ((pan.State == UIGestureRecognizerState.Began || pan.State == UIGestureRecognizerState.Changed) && (pan.NumberOfTouches == 1))
                    {

                        var p0 = pan.LocationInView(this);

                        if (dx == 0)
                            dx = p0.X - cropperView.Origin.X;

                        if (dy == 0)
                            dy = p0.Y - cropperView.Origin.Y;

                        var newX = p0.X - dx;
                        var newY = p0.Y - dy;


                        if (newX < 0)
                            newX = 0;
                        if (newX > Element.Width - cropperView.CropSize.Width)
                            newX = ((nfloat)(Element.Width - cropperView.CropSize.Width));


                        if (newY < 0)
                            newY = 0;

                        var topLeft = imageView.ConvertPointFromView(new CGPoint(newX, newY));
                        if (topLeft.Y < 0)
                            newY = imageView.ConvertPointFromImage(new CGPoint(0, 0)).Y;

                        var rect = imageView.ConvertRectFromView(new CGRect(newX, newY, cropperView.CropRect.Width, cropperView.CropRect.Height));
                        if (rect.Height + rect.Y > imageView.Image.Size.Height)
                            newY = imageView.ConvertPointFromImage(new CGPoint(0, imageView.Image.Size.Height)).Y - cropperView.CropRect.Height;

                        if (newY > Element.Height - cropperView.CropSize.Height)
                            newY = ((nfloat)(Element.Height - cropperView.CropSize.Height));

                        var p1 = new CGPoint(newX, newY);

                        cropperView.Origin = p1;
                    }
                    else if (pan.State == UIGestureRecognizerState.Ended)
                    {
                        dx = 0;
                        dy = 0;
                        UpdateProperies();
                    }
                });

                nfloat s0 = 1;

                pinch = new UIPinchGestureRecognizer(() => {
                    nfloat s = pinch.Scale;
                    nfloat ds = (nfloat)Math.Abs(s - s0);
                    nfloat sf = 0;
                    const float rate = 0.5f;

                    if (s >= s0)
                    {
                        sf = 1 + ds * rate;
                    }
                    else if (s < s0)
                    {
                        sf = 1 - ds * rate;
                    }
                    s0 = s;

                    var rect = imageView.ConvertRectFromView(new CGRect(cropperView.Origin.X, cropperView.Origin.Y, cropperView.CropRect.Width * sf, cropperView.CropRect.Height * sf));

                    if (cropperView.CropSize.Width * sf > (Element.Width - cropperView.Origin.X))
                    {
                        sf = 1;
                    }

                    if (cropperView.CropSize.Height * sf > (Element.Height - cropperView.Origin.Y))
                    {
                        sf = 1;
                    }

                    if (rect.Height > imageView.Image.Size.Height)
                        sf = 1;

                    cropperView.CropSize = new CGSize(cropperView.CropSize.Width * sf, cropperView.CropSize.Height * sf);

                    if (pinch.State == UIGestureRecognizerState.Ended)
                    {
                        s0 = 1;
                        UpdateProperies();
                    }
                });

                doubleTap = new UITapGestureRecognizer((gesture) => Crop())
                {
                    NumberOfTapsRequired = 2,
                    NumberOfTouchesRequired = 1
                };

                cropperView.AddGestureRecognizer(pan);
                cropperView.AddGestureRecognizer(pinch);
                //cropperView.AddGestureRecognizer (doubleTap);    
                UpdateProperies();
            }
            catch (Exception ex)
            {
                Crashes.TrackError(ex);
            }
        }

        void UpdateProperies ()
        {
            var rect = imageView.ConvertRectFromView (cropperView.CropRect);

            var cropView = Element as CropPhotoView;
            cropView.CropTopLeftX = (float)rect.X;
            cropView.CropTopLeftY = (float)rect.Y;
            cropView.CropWidth = (float)rect.Width;
            cropView.CropHeight = (float)rect.Height;
        }

        private void Crop ()
        {
            var inputCGImage = imageView.Image.CGImage;
            var rect = imageView.ConvertRectFromView (cropperView.CropRect);

            var image = inputCGImage.WithImageInRect (rect);
            using (var croppedImage = UIImage.FromImage (image)) {

                imageView.Image = croppedImage;
                imageView.Frame = cropperView.CropRect;
                imageView.Center = NativeView.Center;

                cropperView.Origin = new CGPoint (imageView.Frame.Left, imageView.Frame.Top);
                cropperView.Hidden = true;
            }
        }       
    }
}

iOS
CropperView.cs

public class CropperView : UIView
{
    private const int BorderWidthConstant = 5;
    CGPoint origin;
    CGSize cropSize;

    public CropperView ()
    {
        origin = new CGPoint (100, 100);
        cropSize = new CGSize (250, 250);

        BackgroundColor = UIColor.Clear;
        Opaque = false;
    }

    public CGPoint Origin {
        get {
            return origin;
        }

        set {
            origin = value;
            SetNeedsDisplay ();
        }
    }

    public CGSize CropSize {
        get {
            return cropSize;
        }
        set {
            cropSize = value;
            SetNeedsDisplay ();
        }
    }

    public CGRect CropRect {
        get {
            return new CGRect (Origin, CropSize);
        }
    }

    public override void Draw (CGRect rect)
    {
        base.Draw (rect);

        using (var context = UIGraphics.GetCurrentContext ()) 
        {
            context.SetFillColor (UIColor.FromRGB(31, 33, 36).ColorWithAlpha((System.nfloat)0.3).CGColor);
            context.FillRect (rect);

            context.SetFillColor (UIColor.White.CGColor);
            context.FillRect (new CGRect ( new CGPoint(origin.X - BorderWidthConstant, origin.Y - BorderWidthConstant), new CGSize (cropSize.Width + BorderWidthConstant*2, cropSize.Height + BorderWidthConstant*2)));

            context.SetBlendMode (CGBlendMode.Clear);
            UIColor.Clear.SetColor ();

            var path = new CGPath ();
            path.AddRect (new CGRect (origin, cropSize));

            context.AddPath (path);
            context.DrawPath (CGPathDrawingMode.Fill);
        }
    }
}
Sign In or Register to comment.