Cross Platform Crop Image View

Hey,

Anyone have ever implemented some custom crop image UI that can be used in forms ios/android/win phone?
What I`m trying to achieve is something like instagram that crop a image to a square, so I can store in a db and show it in a circle image after to give my app a better design.
Someone have anything like this? So I will not start from scratch...

Thanks

«1

Posts

  • RaphaelChiorlinRanieriRaphaelChiorlinRanieri BRMember ✭✭✭

    No one have ever implemented something like a cropping UI in forms?
    Please any help would be appreciated.

  • RaphaelChiorlinRanieriRaphaelChiorlinRanieri BRMember ✭✭✭

    @BruceFranson Thanks for the reply!
    Yeah I saw this topic... but what I understand, is that this code only do the "calculations" of a new image based in some boundaries. (Correct me if I'm wrong)
    What I'm having dificult with, is create the view to get this boundaries.
    Like a adjustable transparent square...
    A little bit like this: http://blog.xamarin.com/using-custom-uiviewcontrollers-in-xamarin.forms-on-ios/
    But I would like to do just one view in forms, for cross platform and not a custom native UI for each one...

  • ScottSugdenScottSugden AUMember ✭✭

    Here is a solution that I put together that determines the crop rectangle. It relies on MR.Gestures for all the relevant gestures. In the xaml there are some colour constants that you will need to replace. Use the CropRectangleInPhotoCoordinates property to determine the crop rectangle in photo coordinates. You will then need to implement and call a Dependency service that exposes the actual crop methods on each platform. Let me know how you go.

  • RaphaelChiorlinRanieriRaphaelChiorlinRanieri BRMember ✭✭✭

    @ScottSugden Thanks for the help....
    I tried your code but could not make it work...
    Imported MR.Gesture and take the color constants out, it have compiled and run, but I could not even move the square on the screen...
    Also I could not set the image source, don't know why...

    Can you share a more detailed code/solution when you have some time?
    Would be very clarifying.

    Thank you,
    If any one else have more solution it would be appreciated too.

  • avesaves AFMember
  • ScottSugdenScottSugden AUMember ✭✭

    @RaphaelChiorlinRanieri Without seeing your code it is hard to tell but I have 2 thoughts:
    1. Did you set up and configure Mr.Gestures as per http://www.mrgestures.com/?
    2. Did you use the CropPage constructor that passes in photoWidth, photoHeight and photoToCropBytes?

  • RaphaelChiorlinRanieriRaphaelChiorlinRanieri BRMember ✭✭✭

    @ScottSugden Yeah I used the sample code of mrgestures.com -> https://github.com/MichaelRumpler/GestureSample
    It was case 2 I was using the wrong constructor...I tested in android only but I will attach a zip with solution if someone needs in future...
    However for what I could see you move the picture and not the square, right?
    I would like to move the square and resize it as user needed... But it was a nice approach also!
    Will try to work with it... Thank you very much

    If anyone else have implemented a way which moves the square let me know...
    A very interesting thing I found was this https://github.com/slown1/Xamarin.CircleImageCropper
    But is only for android and not sure if it is free to use...

  • ScottSugdenScottSugden AUMember ✭✭

    @RaphaelChiorlinRanieri. Yes, you are correct. You move the photo and not the square. You can also resize the photo using a pinch etc. (i.e. zoom in/out). This is the way most systems that I have seen work. Let me know if you have any improvements.

  • XamJasXamJas USMember

    @RaphaelChiorlinRanieri any chance you have updated the code you posted? I noticed that when panning around sometimes the image goes back to center for some reason. Thanks!!

  • nerowareneroware USMember ✭✭

    @ScottSugden thanks for the code you uploaded. I'm having trouble understanding how I'm supposed to use the CropRectangleInPhotoCoordinates property in your code. Would you mind providing an example of how to use this rectangle? I've been trying to piece it together and follow your code, but I'm at a loss right now.

  • ScottSugdenScottSugden AUMember ✭✭

    @neroware Here is an example of how you might use it in Android

            /// 
            /// Crops the photo
            /// 
            /// The photo bytes to crop
            /// The rectangle area to crop from the photo
            /// The width of the output photo stream
            /// The height of the output photo stream
            /// 
            public byte[] CropPhoto(byte[] photoToCropBytes, Rectangle rectangleToCrop, double outputWidth, double outputHeight)
            {
                using (var photoOutputStream = new MemoryStream())
                {
                    // Load the bitmap
                    var inSampleSize = CalculateInSampleSize((int)rectangleToCrop.Width, (int)rectangleToCrop.Height, (int)outputWidth, (int)outputHeight);
                    var options = new BitmapFactory.Options();
                    options.InSampleSize = inSampleSize;
                    //options.InPurgeable = true;   see http://developer.android.com/reference/android/graphics/BitmapFactory.Options.html
                    using (var photoToCropBitmap = BitmapFactory.DecodeByteArray(photoToCropBytes, 0, photoToCropBytes.Length, options))
                    {
                        var matrix = new Matrix();
                        var martixScale = outputWidth / rectangleToCrop.Width * inSampleSize;
                        matrix.PostScale((float)martixScale, (float)martixScale);
                        using (var photoCroppedBitmap = Bitmap.CreateBitmap(photoToCropBitmap, (int)(rectangleToCrop.X / inSampleSize), (int)(rectangleToCrop.Y / inSampleSize), (int)(rectangleToCrop.Width / inSampleSize), (int)(rectangleToCrop.Height / inSampleSize), matrix, true))
                        {
                            photoCroppedBitmap.Compress(Bitmap.CompressFormat.Jpeg, 100, photoOutputStream);
                        }
                    }
    
                    return photoOutputStream.ToArray();
                }
            }
    
            /// 
            /// Calculates the sample size value that is a power of two based on a target width and height
            /// 
            /// 
            /// 
            /// 
            /// 
            public static int CalculateInSampleSize(int inputWidth, int inputHeight, int outputWidth, int outputHeight) 
            {
                //see http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
    
                int inSampleSize = 1;       //default
    
                if (inputHeight > outputHeight || inputWidth > outputWidth) {
    
                    int halfHeight = inputHeight / 2;
                    int halfWidth = inputWidth / 2;
    
                    // Calculate the largest inSampleSize value that is a power of 2 and keeps both
                    // height and width larger than the requested height and width.
                    while ((halfHeight / inSampleSize) > outputHeight && (halfWidth / inSampleSize) > outputWidth) 
                    {
                        inSampleSize *= 2;
                    }
                }
    
                return inSampleSize;
            }
    

    And for iOS

            /// 
            /// Crops the photo
            /// 
            /// The photo bytes to crop
            /// The rectangle area to crop from the photo
            /// The width of the output photo stream
            /// The height of the output photo stream
            /// 
            public byte[] CropPhoto(byte[] photoToCropBytes, Xamarin.Forms.Rectangle rectangleToCrop, double outputWidth, double outputHeight)
            {
                byte[] photoOutputBytes;
    
                using (var data = NSData.FromArray(photoToCropBytes))
                {
                    using (var photoToCropCGImage = UIImage.LoadFromData(data).CGImage)
                    {
                        //crop image
                        using (var photoCroppedCGImage = photoToCropCGImage.WithImageInRect(new CGRect((nfloat)rectangleToCrop.X, (nfloat)rectangleToCrop.Y, (nfloat)rectangleToCrop.Width, (nfloat)rectangleToCrop.Height)))
                        {
                            using (var photoCroppedUIImage = UIImage.FromImage(photoCroppedCGImage))
                            {
                                //create a 24bit RGB image to the output size
                                using (var cGBitmapContext = new CGBitmapContext(IntPtr.Zero, (int)outputWidth, (int)outputHeight, 8, (int)(4 * outputWidth), CGColorSpace.CreateDeviceRGB(), CGImageAlphaInfo.PremultipliedFirst))
                                {
                                    var photoOutputRectangleF = new RectangleF(0f, 0f, (float)outputWidth, (float)outputHeight);
    
                                    // draw the cropped photo resized 
                                    cGBitmapContext.DrawImage(photoOutputRectangleF, photoCroppedUIImage.CGImage);
    
                                    //get cropped resized photo
                                    var photoOutputUIImage = UIKit.UIImage.FromImage(cGBitmapContext.ToImage());
    
                                    //convert cropped resized photo to bytes and then stream
                                    using (var photoOutputNsData = photoOutputUIImage.AsJPEG())
                                    {
                                        photoOutputBytes = new Byte[photoOutputNsData.Length];
                                        System.Runtime.InteropServices.Marshal.Copy(photoOutputNsData.Bytes, photoOutputBytes, 0, Convert.ToInt32(photoOutputNsData.Length));
                                    }
                                }
                            }
                        }
                    }
                }
    
                return photoOutputBytes;
            }
    

    and for WP

            /// 
            /// Crops the photo
            /// 
            /// The photo bytes to crop
            /// The rectangle area to crop from the photo
            /// The width of the output photo stream
            /// The height of the output photo stream
            /// 
            public byte[] CropPhoto(byte[] photoToCropBytes, Rectangle rectangleToCrop, double outputWidth, double outputHeight)
            {
                using (var photoOutputStream = new MemoryStream())
                {
    
                    //get writable bitmap to crop
                    var photoToCropBitmap = new BitmapImage();
                    using (var memoryStream = new MemoryStream(photoToCropBytes))
                    {
                        photoToCropBitmap.SetSource(memoryStream);
                    }
    
                    var photoToCropWriteableBitmap = new WriteableBitmap(photoToCropBitmap);
    
                    //get writable bitmap of cropped photo
                    var cropX = (int)rectangleToCrop.X;
                    var cropWidth = (int)rectangleToCrop.Width;
                    var cropY = (int)rectangleToCrop.Y;
                    var cropHeight = (int)rectangleToCrop.Height;
                    var photoCroppedWriteableBitmap = new WriteableBitmap(cropWidth, cropHeight);
                    int i = 0;
                    for (var y = cropY; y < cropY + cropHeight; y++)
                    {
                        for (var x = cropX; x < cropX + cropWidth; x++)
                        {
                            var p = (y * photoToCropWriteableBitmap.PixelWidth) + x;
                            photoCroppedWriteableBitmap.Pixels[i] = photoToCropWriteableBitmap.Pixels[p];
                            i++;
                        }
                    }
    
                    // Encode WriteableBitmap object to a JPEG stream.
                    photoCroppedWriteableBitmap.SaveJpeg(photoOutputStream, (int)outputWidth, (int)outputHeight, 0, 100);
    
                    return photoOutputStream.ToArray();
                }
            }
    

    I hope that this helps. Let me know if you have any improvements.

  • ScottSugdenScottSugden AUMember ✭✭

    @neroware
    To be clear, in each of the above examples you pass in 'CropRectangleInPhotoCoordinates' for 'Rectangle rectangleToCrop'

  • Billy12ShovelsBilly12Shovels USUniversity ✭✭✭

    Hello @ScottSugden
    Did you ever get the "x + width must be <= bitmap.width()"?
    If so, what would you usually pass in for outputHeight and outputWidth
    Thanks

  • ScottSugdenScottSugden AUMember ✭✭

    Hi @CodyRousseau
    I'm not sure what your question is? Can you provide more details?

  • Billy12ShovelsBilly12Shovels USUniversity ✭✭✭
    edited October 2015

    My code is causing an illegalArgumentException on
    Bitmap.CreateBitmap(photoToCropBitmap, (int)(rectangleToCrop.X / inSampleSize), (int)(rectangleToCrop.Y / inSampleSize), (int)(rectangleToCrop.Width / inSampleSize), (int)(rectangleToCrop.Height / inSampleSize), matrix, true))

    with the message "x + width must be <= bitmap.width()".
    So I was wondering what would you recommend to pass in for output width and height compared to your image?
    (ex 212x159)

    I am using your Android method of cropping a photo

    thanks

  • ScottSugdenScottSugden AUMember ✭✭

    @CodyRousseau
    Is your rectangleToCrop correct? It sounds like rectangleToCrop.X + rectangleToCrop.Width is greater than the width of photoToCropBytes?

  • Billy12ShovelsBilly12Shovels USUniversity ✭✭✭

    You pass in CropRectangleInPhotoCoordinates right?

    Also, this only happens when I take a photo, not when I grab one from gallery

  • ScottSugdenScottSugden AUMember ✭✭

    @CodyRousseau
    Yes you do pass in CropRectangleInPhotoCoordinates.
    I have not had this issue. It has been a little while since I have had to work with that code. When I get a chance I will update my project with the latest Xamarin and re-test it on Android when taking a photo.

    Can you tell me the size of your photoToCropBytes (width and height), the dimensions of rectangleToCrop (X, Y, Width and Height), outputWidth and outputHeight when you get the error.

  • Billy12ShovelsBilly12Shovels USUniversity ✭✭✭

    @ScottSugden
    There will be no need; I managed to figure it out on my own. I just need to do a few more tweaks and I'll be good to go!
    Thanks for your input though, and your code samples!

  • ScottSugdenScottSugden AUMember ✭✭

    @CodyRousseau
    No problems.
    If you have any improvements feel free to share them.

  • DanielLDanielL PLInsider ✭✭✭✭
    edited October 2015

    FFImageLoading can do this out of the box: https://github.com/molinch/FFImageLoading (see transformations). Windows Phone not yet ready, but soon.

  • Billy12ShovelsBilly12Shovels USUniversity ✭✭✭

    @DanielL does it convert to streams? I did not see that in the api usage

    @ScottSugden did you have any issues with the android image sometimes being cropped in the wrong position on the picture? Ex. I cropped an image to show only my face but it seemed to move the cropped box, and only get half of my face

  • DanielLDanielL PLInsider ✭✭✭✭
    edited October 2015

    @CodyRousseau If you need to do some custom things you could make your own ITransformation implementation (native for every platform, there are some built-in cropping transformations you could base on). It gives you access to native UIImage(iOS) or Bitmap (Android). After every transformation you could store image data somewhere (e.g. static byte array). Transformations are bindable and don't modify original downloaded files and are called automatically when you change ImageSource or raise its property changed. I'll try to check if we could add a platform independent method to retrieve image data after transformation.

    If you want to use path images source (e.g. from camera roll), use this: https://github.com/daniel-luberda/FFImageLoading/commit/a88e6f7fb3c6104f2846083ee97b47d94ef18da3 (not merged yet)

  • RaphaelChiorlinRanieriRaphaelChiorlinRanieri BRMember ✭✭✭
    edited October 2015

    Hello all,

    If anyone still needs a nice crop for android I ended up using this:
    https://github.com/markuspalme/cropimage-xamarin

    For iOS I did some changes in this code:
    https://github.com/mikebluestein/Cropper
    It is from this link

    My iOS implementation is still very poor so if anyone end up doing a good job let me know.

    However I was not able to find a cross platform implementation like I wanted and had to do it native.

    @DanielL I did not tested your sample yet but it seems like there are just some pre custom crop templates right?
    Is it possible to select the area you want to crop from the image?

  • DanielLDanielL PLInsider ✭✭✭✭
    edited October 2015

    @RaphaelChiorlinRanieri

    No, but you can easily make something like that:

    • make a custom crop transformation, something like that in constructor: CropTransformation(double xOffset, double yOffset, double zoomRatio)
    • pinch/swipe gesture recognizer which sets a new transformation instance for CachedImage (with correct parameters in constructor) after each valid gesture recognized (alternatively you can use some sliders etc)
    • after each gesture recognized, new transformation will be made for an image.

    It's a common used scenario, so I'll try to make a cropper example which use FFImageLoading transformations when I'll have some free time. Will give you a link then. It will be a nice feature to have.

  • @DanielL this entire topic is about making a Crop View... if you could do it would be amazing! :smile:
    for me it was not as simple as it seems :(
    The challenge is to make a good looking View that do not resize the photo.. In fact the photo would be static, and the crop view that would move and resize. In the end it would crop a part of the image that is inside it.
    If you have news let me know! It will help a lot of people not just myself! :smiley:

  • DanielLDanielL PLInsider ✭✭✭✭

    @RaphaelChiorlinRanieri That's what I'm talking about since beginning. Transformations do not resize original photo... only its representation inside the view. If I have some spare time, I'll add it to FFImageLoading examples. The only thing which is missing is how to get byte array of the transformed image in a cross platform way, but It could be added.

  • RaphaelChiorlinRanieriRaphaelChiorlinRanieri BRMember ✭✭✭
    edited October 2015

    @DanielL Yeah it will be a challenge I could not achieve it also. I did it Native for each platform:

    For iOS:

        UIImage imageCropped
        byte[] byteArray = new byte[0];
    
                        using (NSData imageData = imageCropped.AsJPEG ()) {
                            byteArray = new Byte[imageData.Length];
                            System.Runtime.InteropServices.Marshal.Copy (imageData.Bytes, byteArray, 0, Convert.ToInt32 (imageData.Length));
                        }
    

    For Android:

                        Bitmap bmp
                byte[] byteArray = new byte[0];
                using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
                {
                    bmp.Compress(Bitmap.CompressFormat.Jpeg, 75, stream);
    
                    byteArray = stream.ToArray();
                }
    

    Maybe this topic can help:
    http://stackoverflow.com/questions/29122822/convert-xamarin-form-image-type-byte

    Not sure...

    If I can help let me know. ;)

  • ScottSugdenScottSugden AUMember ✭✭

    @CodyRousseau
    No I have not seen that issue.

  • DanielLDanielL PLInsider ✭✭✭✭

    I added CropTransformation to FFImageLoading. In samples it uses buttons (samples are tested with older Forms version which doesn't have PinchGestureRecognizer) but it should be easily migrated to PinchGestureRecognizer.

    On Android I have an issue: https://github.com/molinch/FFImageLoading/issues/91 - will have to find out if it's Xamarin.Forms or FFImageLoading bug (didn't have time for this yet)

    • Basically all you need to do is to set CropTransformation for CachedImage instance with appropriate parameters public CropTransformation(double zoomFactor, double xOffset, double yOffset, double cropWidthRatio, double cropHeightRatio).
    • You can get a jpg or png file after transformation with CachedImage GetImageAsJPG or GetImageAsPNG methods

    Sample code (it's really easy):

    CropTransformationPage.cs
    CropTransformationViewModel.cs

  • DanielLDanielL PLInsider ✭✭✭✭
    edited December 2015

    Just an info: The bug #91 is now fixed :) CropTransformation is working fine on all platforms (iOS/Android/Windows).

  • AlexStrongAlexStrong GBMember ✭✭

    In case this helps anyone I was struggling to get @JamesMontemagno CircleImage working on Android when the source image was not a perfect square. I amended the DrawChild method to the below which solves the problem for me and renders the circle images correctly...

    `protected override bool DrawChild(Canvas canvas, global::Android.Views.View child, long drawingTime)
    {
    try
    {

                ImageView img = (ImageView)child; //Added to fix "non square image" issue
                img.SetScaleType(ImageView.ScaleType.CenterCrop);//Added to fix "non square image" issue
    
                var radius = Math.Min(Width, Height) / 2;
                var strokeWidth = ((CircleImage)Element).BorderThickness;
                radius -= strokeWidth / 2;
    
    
    
    
                var path = new Path();
                path.AddCircle(Width / 2, Height / 2, radius, Path.Direction.Ccw);
    
    
                canvas.Save();
                canvas.ClipPath(path);
    
    
    
                var paint = new Paint();
                paint.AntiAlias = true;
                paint.SetStyle(Paint.Style.Fill);
                paint.Color = ((CircleImage)Element).FillColor.ToAndroid();
                canvas.DrawPath(path, paint);
                paint.Dispose();
    
                var result = base.DrawChild(canvas, img, drawingTime);
    
                canvas.Restore();
    
                path = new Path();
                path.AddCircle(Width / 2, Height / 2, radius, Path.Direction.Ccw);
    
    
                var thickness = ((CircleImage)Element).BorderThickness;
                if(thickness > 0.0f)
                {
                    paint = new Paint();
                    paint.AntiAlias = true;
                    paint.StrokeWidth = thickness;
                    paint.SetStyle(Paint.Style.Stroke);
                    paint.Color = ((CircleImage)Element).BorderColor.ToAndroid();
                    canvas.DrawPath(path, paint);
                    paint.Dispose();
                }
    
                path.Dispose();
                return result;
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine("Unable to create circle image: " + ex);
            }
    
            return base.DrawChild(canvas, child, drawingTime);
        }`
    
  • JamesMontemagnoJamesMontemagno USForum Administrator, Xamarin Team, Developer Group Leader Xamurai

    @AlexStrong in Xamarin.Forms you should just set: Aspect = Aspect.AspectFill, on the Image itself

    No need to modify the code at all.

  • AlexStrongAlexStrong GBMember ✭✭

    Thanks @JamesMontemagno although I most definitely have aspect fill set but I still had issues for some reason, I have no idea why!

  • 14skywalker14skywalker NLMember ✭✭

    Anyone using the CropPage.zip by @ScottSugden. I changed ResizeCropArea() to make it work in landscape as well:

    change://resize overlays var leftRightOverlayWidth = 25; this.cropBoxWidth = contentWidth - leftRightOverlayWidth * 2;

    into://resize overlays this.cropBoxWidth = Math.Min(this.contentWidth, this.contentHeight) - 25 * 2; var leftRightOverlayWidth = (this.contentWidth - cropBoxWidth) / 2;

  • I cannot make working CropTransformation sample of FFImageLoading NuGet package for iOS. Can anyone do?
    Or is there another crop image library for Xamarin.Forms?

  • PaulHazlettPaulHazlett USMember ✭✭

    @DanielL, I took the FFImageLoading sample and added Pan/Pinch gestures and that mostly works but I have one usability problem that the CachedImage only updates after the gesture operation completes. Is there a way to make CachedImage update as the gesture is running?

  • DanielLDanielL PLInsider ✭✭✭✭

    @PaulHazlett Thanks for that, I merged your PR. Yes, the thing is FFImageLoading cancels all previous loading tasks if new one arrives. Gesture recognizer event is called too frequently, so almost every task is cancelled while doing some gestures. We need to implement some delay to that (cancellation tokens + Task.Delay).

  • This is not working DanieIL

Sign In or Register to comment.