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;
        }
 
    }