An ImageCropper control for the Universal Windows Platform

This article introduces an image cropping XAML control for UWP that

  • is populated through a SourceImage dependency property, of type WriteableBitmap,

  • has a templatable UI that allows you to select a region by

    • dragging a corner,

    • dragging the entire selection, and

    • scaling the selection through a pinch gesture,

  • exposes the result as a CroppedImage dependency property, of type WriteableBitmap, and

  • provides helper methods to load and save a WriteableBitmap from and to different image formats.

The sample app shows how to use the control, restyle the control, and capture an image from the camera. Here’s a screenshot of it. It shows the control in its default UI on the left, and the cropped image on the right:

CropperNative

This control is an upgrade of the Windows RT version that I wrote a couple of years ago, with some improvements and bug fixes.

 

Default usage

To use the control, all you need to do is drop it on a page:

<controls:ImageCropper x:Name="ImageCropper" />

It will look like a dull grey rectangle, until you assign a value to its SourceImage property. Here’s how to do that through a file picker (note that the LoadAsync extension method comes with the package):

FileOpenPicker openPicker = new FileOpenPicker();
// ... picker config ...
StorageFile imgFile = await openPicker.PickSingleFileAsync();
if (imgFile != null)
{
    var wb = new WriteableBitmap(1,1);
    await wb.LoadAsync(imgFile);
    this.ImageCropper.SourceImage = wb;
}

The ImageCropper will now show the image, and the UI elements that allow you to select and highlight a region. That selected region is exposed by the control as the CroppedImage property. This is also a WriteableBitmap, so you can use it as a source for an image control:

<Image x:Name="CroppedImage"
       Source="{Binding CroppedImage, ElementName=ImageCropper}"/>

Here’s how to save that cropped image to a file, again through a file picker (and again: the SaveAsync extension method comes with the package):

FileSavePicker savePicker = new FileSavePicker();
// ... picker config ...
var file = await savePicker.PickSaveFileAsync();
if (file != null)
{
    await (this.CroppedImage.Source as WriteableBitmap).SaveAsync(file);
}

All common image formats are supported: *.bmp, *.gif, *.jpg, *.png, *.tiff and more. 

 

Customizing the UI

The entire control is templatable, but the corner of the selected region is the ideal candidate for customization. If you don’t like the default ellipse, then you can go for a more angular look:

CropperTemplated

You have to provide only the template for the upper left corner, the control’s default style reuses it and applies a rotation. Here’s the code for the upper right corner:

<ContentControl ContentTemplate="{StaticResource CornerTemplate}"
                x:Name="PART_TopRightCorner"
                Tag="TopRightCorner"
                Canvas.Left="{Binding Path=BottomRightCornerCanvasLeft,Mode=OneWay}"
                Canvas.Top="{Binding Path=TopLeftCornerCanvasTop,Mode=OneWay}">
    <ContentControl.RenderTransform>
        <RotateTransform Angle="90" />
    </ContentControl.RenderTransform>
</ContentControl>

Here’s the custom template for the corner element. I’s a group of two RectangleGeometry instances, that is translated out of the selected region. Since these are hard to hit by touch, there’s a transparent ellipse on top of them:

<DataTemplate x:Key="CornerTemplate">
    <Grid>
        <Path Fill="{StaticResource ApplicationPageBackgroundThemeBrush}"
              Stroke="{StaticResource ApplicationForegroundThemeBrush}"
              StrokeThickness="0">
            <Path.Data>
                <GeometryGroup FillRule="Nonzero">
                    <RectangleGeometry Rect="0 0 8 40" />
                    <RectangleGeometry Rect="0 0 40 8" />
                </GeometryGroup>
            </Path.Data>
            <Path.RenderTransform>
                <CompositeTransform TranslateX="-12"
                                    TranslateY="-12" />
            </Path.RenderTransform>
        </Path>

        <!--To get a better grip-->
        <Ellipse Height="40"
                  Width="40"
                  Fill="Transparent"
                  StrokeThickness="0">
            <Ellipse.RenderTransform>
                <CompositeTransform TranslateX="-12"
                                    TranslateY="-12" />
            </Ellipse.RenderTransform>
        </Ellipse>
    </Grid>
</DataTemplate>

 

Capturing the camera view

A common use case for this type of control is the selection of a portion of the image from a camera built into the device or attached to it. For this scenario, I placed a CaptureElement on top of the ImageCropper, to display the stream from the (back) camera:

<controls:ImageCropper x:Name="ImageCropper" />
<CaptureElement x:Name="CameraPreview" />

I refactored the code of the new Camera Preview Frame sample (see Credits) into a reusable MVVM-ish CameraService class. When the user navigates to the corresponding page, the service looks up the camera, configures it, and hooks its image stream to the CaptureElement:

private CameraService cameraService;

public CameraPage()
{
    this.InitializeComponent();

    cameraService = new CameraService(this.CameraPreview);
}

protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    await cameraService.InitializeCameraAsync();
    base.OnNavigatedTo(e);
}

When the user takes the picture, a temporary file is created, and the current camera frame is saved to it. The file is then loaded again, and provided to the ImageCropper:

StorageFolder temp = ApplicationData.Current.LocalCacheFolder;
StorageFile file = await temp.CreateFileAsync("current_image.png", CreationCollisionOption.ReplaceExisting);

await cameraService.MediaCapture.CapturePhotoToStorageFileAsync(ImageEncodingProperties.CreateJpeg(), file);

file = null;
file = await temp.GetFileAsync("current_image.png");

var wb = new WriteableBitmap(1, 1);
await wb.LoadAsync(file);
this.ImageCropper.SourceImage = wb;

await cameraService.StopPreviewAsync();
this.CameraPreview.Visibility = Visibility.Collapsed;

[The file = null is necessary, to unlock the file between the overriding and the reading.]

Here’s the ImageCropper control cropping some flowers from my garden:

CameraCapture

 

Credits

The ImageCropper control was originally written according to the Hackathon Design Pattern: it was quickly assembled from different non-related resources on the Internet, just to win a device. But don’t worry: I took the time to refactor and debug it properly. Here are some of the resources:

 

Source Code

The control and the sample app live here on GitHub. The solution was written using Visual Studio 2015 RC, and then manually upgraded to Visual Studio 2015 RTM.

Enloy!

XAML Brewer