Tuesday 4 March 2014

Image crop control for Silverlight or Windows Phone

This example gives the basis of an image cropping control for Silverlight or Windows Phone (7 or 8). It produces output as left, right, top and bottom trim %s for generic use. It would be trivial to perform the actual trimming with a library such as WriteableBitmapEx. I have made no effort to tidy this code to be more efficient or readable, but there are many such modifications that could be made.

This code allows for the phone to be used in either portrait or landscape mode. Aspect ratio is not locked, but the code could be tweaked to enforce this.

Try it out here in my WP8 Meme Maker app.


How it works
Two images sit within a grid, with the semi-transparent instance behind in z-order. A rectangle is positioned in each corner. The topmost image is clipped to a rectangle defined by dragging the rectangles around.


XAML:


<Grid>
 
            <Grid  HorizontalAlignment="Center" VerticalAlignment="Center">
 
                <Image Source="Assets/someImage.png" IsHitTestVisible="False" Opacity="0.3" ></Image>
                <Image Name="imgSauce" Source="Assets/someImage.png" >
                    <Image.Clip>
                        <RectangleGeometry x:Name="clipRect" ></RectangleGeometry>
                    </Image.Clip>
                </Image>
                <Rectangle Name="rectTopLeft" Width="20" Height="20" Margin="-10" Fill="Yellow" HorizontalAlignment="Left" VerticalAlignment="Top" >
                    <Rectangle.RenderTransform>
                        <TranslateTransform></TranslateTransform>
                    </Rectangle.RenderTransform>
                </Rectangle>
                <Rectangle Name="rectTopRight" Width="20" Height="20" Margin="-10" Fill="Yellow" HorizontalAlignment="Right" VerticalAlignment="Top" >
                    <Rectangle.RenderTransform>
                        <TranslateTransform></TranslateTransform>
                    </Rectangle.RenderTransform>
                </Rectangle>
                <Rectangle Name="rectBotLeft" Width="20" Height="20" Margin="-10" Fill="Yellow" HorizontalAlignment="Left" VerticalAlignment="Bottom" >
                    <Rectangle.RenderTransform>
                        <TranslateTransform></TranslateTransform>
                    </Rectangle.RenderTransform>
                </Rectangle>
                <Rectangle Name="rectBotRight" Width="20" Height="20" Margin="-10" Fill="Yellow" HorizontalAlignment="Right" VerticalAlignment="Bottom" >
                    <Rectangle.RenderTransform>
                        <TranslateTransform></TranslateTransform>
                    </Rectangle.RenderTransform>
                </Rectangle>
            </Grid>
        </Grid>

Code Behind:

using System;
using System.Windows;
using Microsoft.Phone.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

public partial class CropPage : Page
    {
 
        private Rectangle _draggedRect = null;
        
 
        public CropPage()
        {
            InitializeComponent();
 
            
            var rects = new Rectangle[] { rectTopRight, rectTopLeft, rectBotRight, rectBotLeft };
            Point _dragOrigin =new Point();
            double origLeftPerc= 0, origRightPerc = 0, origTopPerc = 0, origBotPerc = 0;
 
            var setOrigin = new Action<Point>((p) => {
                _dragOrigin = p;
                    origLeftPerc = this._clipLeftPerc;
                    origRightPerc = this._clipRightPerc;
                    origTopPerc = this._clipTopPerc;
                    origBotPerc = this._clipBotPerc;
            });
 
            foreach (var aRect in rects)
            {
                aRect.MouseLeftButtonDown += (s, e) => {
                    var r = (Rectangle)s;
                    _draggedRect = r;
                    setOrigin( e.GetPosition(this.imgSauce));
                    
                    r.CaptureMouse();
                };
 
                aRect.MouseLeftButtonUp += (s, e) => {                    
                    _draggedRect = null;
                };
 
                aRect.MouseMove += (s, e) => {
                    if (_draggedRect != null) {
 
                        var pos = e.GetPosition(this.imgSauce);
 
                        if (s == this.rectTopLeft || s == this.rectTopRight) {
                            // Adjust top
                            _clipTopPerc = origTopPerc + (pos.Y - _dragOrigin.Y) / imgSauce.ActualHeight;
                        }
                        if (s == this.rectTopLeft || s == this.rectBotLeft) {
                            // Adjust Left
                            _clipLeftPerc = origLeftPerc + (pos.X - _dragOrigin.X) / imgSauce.ActualWidth;
                        }
                        if (s == this.rectBotLeft || s == this.rectBotRight) {
                            // Adjust bottom
                            _clipBotPerc = origBotPerc - (pos.Y - _dragOrigin.Y) / imgSauce.ActualHeight;
                        }
                        if (s == this.rectTopRight || s == this.rectBotRight) {
                            // Adjust Right
                            _clipRightPerc = origRightPerc - (pos.X - _dragOrigin.X) / imgSauce.ActualWidth;
                        }
 
                        this.updateClipAndTransforms();
                    }
                };
            }
 
            var draggingImg = false;
 
            imgSauce.MouseLeftButtonDown += (s, e) => {
               setOrigin( e.GetPosition(this.imgSauce));
                imgSauce.CaptureMouse();
                draggingImg = true;
            };
 
            imgSauce.MouseLeftButtonUp += (s, e) => {
                draggingImg = false;
            };
 
            imgSauce.MouseMove += (s, e) => {
                if (draggingImg) {
                    var pos = e.GetPosition(this.imgSauce);
 
                    var xAdjust = (pos.X - _dragOrigin.X) / imgSauce.ActualWidth;
                    var yAdjust = (pos.Y - _dragOrigin.Y) / imgSauce.ActualHeight;
 
                    _clipLeftPerc = origLeftPerc + xAdjust;
                    _clipRightPerc = origRightPerc - xAdjust;
                    _clipTopPerc = origTopPerc + yAdjust;
                    _clipBotPerc = origBotPerc - yAdjust;
 
                    this.updateClipAndTransforms();
                }
            };
 
            imgSauce.SizeChanged += (x,y) => {
                this.updateClipAndTransforms();
            };
 
            this.updateClipAndTransforms();
        }
 
 
 
        private double _clipLeftPerc, _clipRightPerc, _clipTopPerc, _clipBotPerc =  0;
 
        void updateClipAndTransforms()
        {
            // Check bounds
            if (_clipLeftPerc + _clipRightPerc >= 1)
                _clipLeftPerc = (1 - _clipRightPerc) - 0.04;
            if (_clipTopPerc + _clipBotPerc >= 1)
                _clipTopPerc = (1 - _clipBotPerc) - 0.04;
 
            if (_clipLeftPerc < 0)
                _clipLeftPerc = 0;
            if (_clipRightPerc < 0)
            _clipRightPerc = 0;
            if (_clipBotPerc < 0)
                _clipBotPerc = 0;
            if (_clipTopPerc < 0)
                _clipTopPerc = 0;
            if (_clipLeftPerc >= 1)
                _clipLeftPerc = 0.99;
            if (_clipRightPerc >= 1)
                _clipRightPerc = 0.99;
            if (_clipBotPerc >= 1)
                _clipBotPerc = 0.99;
            if (_clipTopPerc >= 1)
                _clipTopPerc = 0.99;
            
 
            // Image Clip
            var leftX = _clipLeftPerc * this.imgSauce.ActualWidth;
            var topY = _clipTopPerc * this.imgSauce.ActualHeight;
            
            clipRect.Rect = new Rect(leftX, topY, (1 -_clipRightPerc) * this.imgSauce.ActualWidth - leftX, (1 - _clipBotPerc) *  this.imgSauce.ActualHeight - topY);
 
            // Rectangle Transforms
            ((TranslateTransform)this.rectTopLeft.RenderTransform).X = clipRect.Rect.X;
            ((TranslateTransform)this.rectTopLeft.RenderTransform).Y = clipRect.Rect.Y;
            ((TranslateTransform)this.rectTopRight.RenderTransform).X = -_clipRightPerc * this.imgSauce.ActualWidth;
            ((TranslateTransform)this.rectTopRight.RenderTransform).Y = clipRect.Rect.Y;
            ((TranslateTransform)this.rectBotLeft.RenderTransform).X = clipRect.Rect.X;
            ((TranslateTransform)this.rectBotLeft.RenderTransform).Y = - _clipBotPerc *  this.imgSauce.ActualHeight;
            ((TranslateTransform)this.rectBotRight.RenderTransform).X = -_clipRightPerc * this.imgSauce.ActualWidth;
            ((TranslateTransform)this.rectBotRight.RenderTransform).Y = -_clipBotPerc * this.imgSauce.ActualHeight;
        }
 
    }

5 comments:

Naga Harish Movva said...

Really nice

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

Thank you very much. I have been trying to accomplish this for some time now with manual cropping. But the clipping solution works a lot smoother.

Anonymous said...

this is really interesting, as I'm searching for a way to resize and crop images at the moment.

You don't happen to have a nice way to resize/crop images for c# Windows Phone 8.1? Because that would be really awesome ... (I even had a look into the new Nokia Imaging Lib 1.2, but no luck there...)

Lachlan Keown said...

Anon, to resize/crop in WP 8.1, use Nuget to import the WriteableBitmapEx package, and then use Crop() and Resize() functions, it's pretty straightforward. Contact me directly using contact form if you need more help. Here's how you can use Crop() on a WriteableBitmap after user has finished cropping in above control (in VB.Net):

ImageToCrop.Crop(ImageToCrop.PixelWidth * _clipLeftPerc, ImageToCrop.PixelHeight * _clipTopPerc, ImageToCrop.PixelWidth * (1 - (_clipLeftPerc + _clipRightPerc)), ImageToCrop.PixelHeight * (1 - (_clipTopPerc + _clipBotPerc)))