Click here to Skip to main content
15,896,153 members
Articles / Desktop Programming / WPF

WPF Interactive Image Cropping Control

Rate me:
Please Sign up or sign in to vote.
4.90/5 (60 votes)
6 Sep 20078 min read 413.9K   11.6K   146  
WPF Interactive Image Cropping Control
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.IO;

#region Explanation of why this .NET3.0 app is using .NET2.0 Dlls
//For some very simple .NET niceties like being able to save a bitmap 
//to a filename I have had to use the System.Drawing .NET 2.0 Dll
//
//While this looks possible using something like the following :
//
//RenderTargetBitmap rtb = new RenderTargetBitmap((int)img.width,
//(int)img, 0, 0, PixelFormats.Default);
//rtb.Render(this.inkCanv);
//BmpBitmapEncoder encoder = new BmpBitmapEncoder();
//encoder.Frames.Add(BitmapFrame.Create(rtb));
//encoder.Save(file);
//file.Close();
//
//For this to work I would have needed to used a .NET 3.0 CroppedBitmap
//within the RenderTargetBitmap.Render() method. And as CroppedBitmap
//doesnt inherit from Visual this is not possible.
//
//So if anyone knows how to do this better in .NET 3.0 I am all ears
#endregion
using System.Drawing;
using System.Drawing.Drawing2D;

//Josh Smith excellent DragCanvas
using WPF.JoshSmith.Controls;

namespace ImageCropper
{

    /// <summary>
    /// Provides a simple Image cropping facility for a WPF image element,
    /// where the cropped area may be picked using a rubber band and moved
    /// by dragging the rubber band around the image. There is also a popup
    /// window from where the user may accept or reject the crop.
    /// </summary>
    public partial class UcImageCropper : System.Windows.Controls.UserControl
    {

        #region CropperStyle Dependancy property

        /// <summary>
        /// A DP for the Cropp Rectangle Style
        /// </summary>
        public Style CropperStyle
        {
            get { return (Style)GetValue(CropperStyleProperty); }
            set { SetValue(CropperStyleProperty, value); }
        }

        /// <summary>
        /// register the DP
        /// </summary>
        public static readonly DependencyProperty CropperStyleProperty =
            DependencyProperty.Register(
            "CropperStyle",
            typeof(Style),
            typeof(UcImageCropper),
            new UIPropertyMetadata(null, new PropertyChangedCallback(OnCropperStyleChanged)));

        /// <summary>
        /// The callback that actually changes the Style if one was provided
        /// </summary>
        /// <param name="depObj">UcImageCropper</param>
        /// <param name="e">The event args</param>
        static void OnCropperStyleChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            Style s = e.NewValue as Style;
            if (s != null)
            {
                UcImageCropper uc = (UcImageCropper)depObj;
                uc.selectCanvForImg.CropperStyle = s;
            }
        }
        #endregion

        #region Instance fields
        private string ImgUrl = "";
        private BitmapImage bmpSource = null;
        private SelectionCanvas selectCanvForImg = null;
        private DragCanvas dragCanvasForImg = null;
        private System.Windows.Controls.Image img = null;
        private Shape rubberBand;
        private double rubberBandLeft;
        private double rubberBandTop;
        private string tempFileName;
        private ContextMenu cmSelectionCanvas;
        private RoutedEventHandler cmSelectionCanvasRoutedEventHandler;
        private ContextMenu cmDragCanvas;
        private RoutedEventHandler cmDragCanvasRoutedEventHandler;
        private string fixedTempName = "temp";
        private long fixedTempIdx = 1;
        private double zoomFactor=1.0;
        #endregion

        #region Ctor
        public UcImageCropper()
        {
            InitializeComponent();

            //this.Unloaded += new RoutedEventHandler(UcImageCropper_Unloaded);
            selectCanvForImg = new SelectionCanvas();
            selectCanvForImg.CropImage += new RoutedEventHandler(selectCanvForImg_CropImage);
            dragCanvasForImg = new DragCanvas();





        }


        #endregion

        #region Public properties
        public string ImageUrl
        {
            get { return this.ImgUrl; }
            set
            {
                zoomFactor = 1.0;
                ImgUrl = value;
                grdCroppedImage.Visibility = Visibility.Hidden;
                createImageSource();
                createSelectionCanvas();
                //apply the default style if the user of this control didnt supply one
                if (CropperStyle == null)
                {
                    Style s = gridMain.TryFindResource("defaultCropperStyle") as Style;
                    if (s != null)
                    {
                        CropperStyle = s;
                    }
                }

            }
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Deletes all occurences of previous unused temp files from the
        /// current temporary path
        /// </summary>
        /// <param name="tempPath">The temporary file path</param>
        /// <param name="fixedTempName">The file name part to search for</param>
        /// <param name="CurrentFixedTempIdx">The current temp file suffix</param>
        public void CleanUp(string tempPath, string fixedTempName, long CurrentFixedTempIdx)
        {
            //clean up the single temporary file created
            try
            {
                string filename = "";
                for (int i = 0; i < CurrentFixedTempIdx; i++)
                {
                    filename = tempPath + fixedTempName + i.ToString() + ".jpg";
                    File.Delete(filename);
			    }
            }
            catch (Exception)
            {
            }
        }

        /// <summary>
        /// Popup form Cancel clicked, so created the SelectionCanvas to start again
        /// </summary>
        private void btnCancel_Click(object sender, RoutedEventArgs e)
        {
            grdCroppedImage.Visibility = Visibility.Hidden;
            createSelectionCanvas();
        }

        /// <summary>
        /// Popup form Confirm clicked, so save the file to their desired location
        /// </summary>
        private void btnConfirm_Click(object sender, RoutedEventArgs e)
        {
            ImageUrl = tempFileName;
            grdCroppedImage.Visibility = Visibility.Hidden;
        }

        /// <summary>
        /// creates the selection canvas, where user can draw
        /// selection rectangle
        /// </summary>
        private void createSelectionCanvas()
        {
            createImageSource();
            selectCanvForImg.Width = bmpSource.Width * zoomFactor;
            selectCanvForImg.Height = bmpSource.Height * zoomFactor;
            selectCanvForImg.Children.Clear();
            selectCanvForImg.rubberBand = null;
            selectCanvForImg.Children.Add(img);
            svForImg.Width = selectCanvForImg.Width;
            svForImg.Height = selectCanvForImg.Height;
            svForImg.Content = selectCanvForImg;
            createSelectionCanvasMenu();
        }

        /// <summary>
        /// Creates the selection canvas context menu
        /// </summary>
        private void createSelectionCanvasMenu()
        {
            cmSelectionCanvas = new ContextMenu();
            MenuItem miZoom25 = new MenuItem();
            miZoom25.Header = "Zoom 25%";
            miZoom25.Tag = "0.25";
            MenuItem miZoom50 = new MenuItem();
            miZoom50.Header = "Zoom 50%";
            miZoom50.Tag = "0.5";
            MenuItem miZoom100 = new MenuItem();
            miZoom100.Header = "Zoom 100%";
            miZoom100.Tag = "1.0";
            cmSelectionCanvas.Items.Add(miZoom25);
            cmSelectionCanvas.Items.Add(miZoom50);
            cmSelectionCanvas.Items.Add(miZoom100);
            cmSelectionCanvasRoutedEventHandler = new RoutedEventHandler(MenuSelectionCanvasOnClick);
            cmSelectionCanvas.AddHandler(MenuItem.ClickEvent, cmSelectionCanvasRoutedEventHandler);
            selectCanvForImg.ContextMenu = cmSelectionCanvas;
        }

        /// <summary>
        /// Handles the selction canvas context menu. Which will zoom the
        /// current image to either 25,50 or 100%
        /// </summary>
        private void MenuSelectionCanvasOnClick(object sender, RoutedEventArgs args)
        {
            MenuItem item = args.Source as MenuItem;
            zoomFactor = double.Parse(item.Tag.ToString());
            img.RenderTransform = new ScaleTransform(zoomFactor, zoomFactor, 0.5, 0.5);
            selectCanvForImg.Width = bmpSource.Width * zoomFactor;
            selectCanvForImg.Height = bmpSource.Height * zoomFactor;
            svForImg.Width = selectCanvForImg.Width;
            svForImg.Height = selectCanvForImg.Height;

        }

        /// <summary>
        /// Creates the Image source for the current canvas
        /// </summary>
        private void createImageSource()
        {
            bmpSource = new BitmapImage(new Uri(ImgUrl));
            img = new System.Windows.Controls.Image();
            img.Source = bmpSource;
            //if there was a Zoom Factor applied
            img.RenderTransform = new ScaleTransform(zoomFactor, zoomFactor, 0.5, 0.5);
        }

        /// <summary>
        /// creates the drag canvas, where user can drag the
        /// selection rectangle
        /// </summary>
        private void createDragCanvas()
        {
            dragCanvasForImg.Width = bmpSource.Width * zoomFactor;
            dragCanvasForImg.Height = bmpSource.Height * zoomFactor;
            svForImg.Width = dragCanvasForImg.Width;
            svForImg.Height = dragCanvasForImg.Height;
            createImageSource();
            createDragCanvasMenu();
            selectCanvForImg.Children.Remove(rubberBand);
            dragCanvasForImg.Children.Clear();
            dragCanvasForImg.Children.Add(img);
            dragCanvasForImg.Children.Add(rubberBand);
            svForImg.Content = dragCanvasForImg;
        }

        /// <summary>
        /// Creates the drag canvas context menu
        /// </summary>
        private void createDragCanvasMenu()
        {
            cmSelectionCanvas.RemoveHandler(MenuItem.ClickEvent, cmSelectionCanvasRoutedEventHandler);
            selectCanvForImg.ContextMenu = null;
            cmSelectionCanvas = null;
            cmDragCanvas = new ContextMenu();
            MenuItem miCancel = new MenuItem();
            miCancel.Header = "Cancel";
            MenuItem miSave = new MenuItem();
            miSave.Header = "Save";
            cmDragCanvas.Items.Add(miCancel);
            cmDragCanvas.Items.Add(miSave);
            cmDragCanvasRoutedEventHandler = new RoutedEventHandler(MenuDragCanvasOnClick);
            cmDragCanvas.AddHandler(MenuItem.ClickEvent, cmDragCanvasRoutedEventHandler);
            dragCanvasForImg.ContextMenu = cmDragCanvas;
        }

        /// <summary>
        /// Handles the selction drag context menu. Which allows user to cancel or save 
        /// the current croped area
        /// </summary>
        private void MenuDragCanvasOnClick(object sender, RoutedEventArgs args)
        {
            MenuItem item = args.Source as MenuItem;
            switch (item.Header.ToString())
            {
                case "Save":
                    SaveCroppedImage();
                    break;
                case "Cancel":
                    createSelectionCanvas();
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// Raised by the <see cref="selectionCanvas">selectionCanvas</see>
        /// when the new crop shape (rectangle) has been drawn. This event
        /// then replaces the current selectionCanvas with a <see cref="DragCanvas">DragCanvas</see>
        /// which can then be used to drag the crop area around within a Canvas
        /// </summary>
        private void selectCanvForImg_CropImage(object sender, RoutedEventArgs e)
        {
            rubberBand = (Shape)selectCanvForImg.Children[1];
            createDragCanvas();
        }

        /// <summary>
        /// User cancelled out of the popup, so go back to showing original image
        /// </summary>
        private void lblExit_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            grdCroppedImage.Visibility = Visibility.Hidden;
            createSelectionCanvas();
        }

        /// <summary>
        /// Saves the cropped image area to a temp file, and shows a confirmation
        /// popup from where the user may accept or reject the cropped image.
        /// If they accept the new cropped image will be used as the new image source
        /// for the current canvas, if they reject the crop, the existing image will
        /// be used for the current canvas
        /// </summary>
        private void SaveCroppedImage()
        {
            if (popUpImage.Source!=null)
                popUpImage.Source = null; 

            try
            {
                rubberBandLeft = Canvas.GetLeft(rubberBand);
                rubberBandTop = Canvas.GetTop(rubberBand);
                //create a new .NET 2.0 bitmap (which allowing saving) based on the bound bitmap url
                using (System.Drawing.Bitmap source = new System.Drawing.Bitmap(ImgUrl))
                {
                    //create a new .NET 2.0 bitmap (which allowing saving) to store cropped image in, should be 
                    //same size as rubberBand element which is the size of the area of the original image we want to keep
                    using (System.Drawing.Bitmap target = new System.Drawing.Bitmap((int)rubberBand.Width, (int)rubberBand.Height))
                    {
                        //create a new destination rectange
                        System.Drawing.RectangleF recDest = new System.Drawing.RectangleF(0.0f, 0.0f, (float)target.Width, (float)target.Height);
                        //different resolution fix prior to cropping image
                        float hd = 1.0f / (target.HorizontalResolution / source.HorizontalResolution);
                        float vd = 1.0f / (target.VerticalResolution / source.VerticalResolution);
                        float hScale = 1.0f / (float)zoomFactor;
                        float vScale = 1.0f / (float)zoomFactor;
                        System.Drawing.RectangleF recSrc = new System.Drawing.RectangleF((hd * (float)rubberBandLeft) * hScale, (vd * (float)rubberBandTop) * vScale, (hd * (float)rubberBand.Width) * hScale, (vd * (float)rubberBand.Height) * vScale);
                        using (System.Drawing.Graphics gfx = System.Drawing.Graphics.FromImage(target))
                        {
                            gfx.DrawImage(source, recDest, recSrc, System.Drawing.GraphicsUnit.Pixel);
                        }
                        //create a new temporary file, and delete all old ones prior to this new temp file
                        //This is is a hack that I had to put in, due to GDI+ holding on to previous 
                        //file handles used by the Bitmap.Save() method the last time this method was run.
                        //This is a well known issue see http://support.microsoft.com/?id=814675 for example
                        tempFileName = System.IO.Path.GetTempPath();
                        if (fixedTempIdx > 2)
                            fixedTempIdx = 0;
                        else
                            ++fixedTempIdx;
                        //do the clean
                        CleanUp(tempFileName, fixedTempName, fixedTempIdx);
                        //Due to the so problem above, which believe you me I have tried and tried to resolve
                        //I have tried the following to fix this, incase anyone wants to try it
                        //1. Tried reading the image as a strea of bytes into a new bitmap image
                        //2. I have tried to use teh WPF BitmapImage.Create()
                        //3. I have used the trick where you use a 2nd Bitmap (GDI+) to be the newly saved
                        //   image
                        //
                        //None of these worked so I was forced into using a few temp files, and pointing the 
                        //cropped image to the last one, and makeing sure all others were deleted.
                        //Not ideal, so if anyone can fix it please this I would love to know. So let me know
                        tempFileName = tempFileName + fixedTempName + fixedTempIdx.ToString() + ".jpg";
                        target.Save(tempFileName, System.Drawing.Imaging.ImageFormat.Jpeg);
                        //rewire up context menu
                        cmDragCanvas.RemoveHandler(MenuItem.ClickEvent, cmDragCanvasRoutedEventHandler);
                        dragCanvasForImg.ContextMenu = null;
                        cmDragCanvas = null;
                        //create popup BitmapImage
                        BitmapImage bmpPopup = new BitmapImage(new Uri(tempFileName));
                        popUpImage.Source = bmpPopup;
                        grdCroppedImage.Visibility = Visibility.Visible;
                    }
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions