Bitmap > Working with Bitmap for Silverlight > Cropping with a Draggable Crop Box |
Being able to crop an image entirely on the client is a highly useful task. Thankfully, with C1Bitmap or the WriteableBitmap class (introduced in Silverlight 3) this is achievable in Silverlight. The C1Bitmap component provides an API that is easier to work with when doing any bitmap related manipulation. Primarily because it can get and set simple colors and it gives more direct access to pixels with the GetPixel and SetPixel methods.
While C1Bitmap provides us the API needed to crop the image, it does not however provide us the UI. There are countless ways to implement an image cropping UI. I think most developers and image editors alike prefer to have a draggable box with adorners. This is commonly seen in professional image editing software such as Adobe Photoshop. So that's what we will create.
Here is the XAML that defines the elements needed to create our crop box. It consists of 4 Thumbs which the user can drag, and 4 shaded rectangles which mask the regions that will be cropped out. We place all of these elements in a Canvas so we can easily adjust the positions in code.
XAML |
Copy Code
|
---|---|
<Canvas Name="cropCanvas"> <Rectangle Name="topMask" Fill="{StaticResource MaskBrush}" Canvas.Top="0" Canvas.Left="0" /> <Rectangle Name="bottomMask" Fill="{StaticResource MaskBrush}" /> <Rectangle Name="leftMask" Fill="{StaticResource MaskBrush}" Canvas.Left="0"/> <Rectangle Name="rightMask" Fill="{StaticResource MaskBrush}" Canvas.Top="0" /> <Thumb Name="cropUL" Width="10" Height="10" DragDelta="cropUL_DragDelta" Cursor="SizeNWSE" /> <Thumb Name="cropUR" Width="10" Height="10" DragDelta="cropUR_DragDelta" Cursor="SizeNESW" /> <Thumb Name="cropBL" Width="10" Height="10" DragDelta="cropBL_DragDelta" Cursor="SizeNESW" /> <Thumb Name="cropBR" Width="10" Height="10" DragDelta="cropBR_DragDelta" Cursor="SizeNWSE" /> </Canvas> |
The code needed to manipulate the crop box is quite complex. It's possible to implement a draggable crop box using behaviors and the Visual State Manager, but a coded solution is definitely easier to understand for novice Silverlight developers. The purpose of the crop box UI is to generate a simple Rect which will be used by the C1Bitmap to determine the coordinates of the cropping. By clicking the "Crop" button on the toolbar we will display the crop box at full size. As the user drags the adorners we utilize each Thumb's DragDelta event to capture the vertical and horizontal change. Then with a bit of logic and simple math, we can manipulate the behavior of the other adorners which the user is not dragging. To complete the cropping action, the user simply clicks the Crop button again (it's a C1ToolbarToggleButton).
C# |
Copy Code
|
---|---|
private void cropUL_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { double left = Canvas.GetLeft(cropUL) + e.HorizontalChange; double top = Canvas.GetTop(cropUL) + e.VerticalChange; if (left > 0 && left < bitmap.Width && cropBox.Width > e.HorizontalChange) { cropBox = new Rect(left, cropBox.Top, cropBox.Width - e.HorizontalChange, cropBox.Height); } if (top > 0 && top < bitmap.Height && cropBox.Height > e.VerticalChange) { cropBox = new Rect(cropBox.Left, top, cropBox.Width, cropBox.Height - e.VerticalChange); } UpdateCropBox(); } private void cropUR_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { double left = Canvas.GetLeft(cropUR) + e.HorizontalChange; double top = Canvas.GetTop(cropUR) + e.VerticalChange; if (left > 0 && left < bitmap.Width && left > cropBox.Left) { cropBox = new Rect(cropBox.Left, cropBox.Top, left - cropBox.Left, cropBox.Height); } if (top > 0 && top < bitmap.Height && cropBox.Height > e.VerticalChange) { cropBox = new Rect(cropBox.Left, top, cropBox.Width, cropBox.Height - e.VerticalChange); } UpdateCropBox(); } private void cropBL_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { double left = Canvas.GetLeft(cropBL) + e.HorizontalChange; double top = Canvas.GetTop(cropBL) + e.VerticalChange; if (left > 0 && left < bitmap.Width && cropBox.Width > e.HorizontalChange) { cropBox = new Rect(left, cropBox.Top, cropBox.Width - e.HorizontalChange, cropBox.Height); } if (top > 0 && top < bitmap.Height && top > cropBox.Top) { cropBox = new Rect(cropBox.Left, cropBox.Top, cropBox.Width, top - cropBox.Top); } UpdateCropBox(); } private void cropBR_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) { double left = Canvas.GetLeft(cropBR) + e.HorizontalChange; double top = Canvas.GetTop(cropBR) + e.VerticalChange; if (left > 0 && left < bitmap.Width && left > cropBox.Left) { cropBox = new Rect(cropBox.Left, cropBox.Top, left - cropBox.Left, cropBox.Height); } if (top > 0 && top < bitmap.Height && cropBox.Height + e.VerticalChange > 0) { cropBox = new Rect(cropBox.Left, cropBox.Top, cropBox.Width, cropBox.Height + e.VerticalChange); } UpdateCropBox(); } |
We apply some logic for the bounds of eachadorner in the "if" statements above. For example, you should not be able to drag the bottom-right adorner further left beyond the bottom-left adorner and so on. And the adorners should not be draggable outside the bounds of the image.
C# |
Copy Code
|
---|---|
private void UpdateCropBox() { Canvas.SetLeft(cropUL, cropBox.Left); Canvas.SetTop(cropUL, cropBox.Top); Canvas.SetLeft(cropUR, cropBox.Left + cropBox.Width); Canvas.SetTop(cropUR, cropBox.Top); Canvas.SetLeft(cropBL, cropBox.Left); Canvas.SetTop(cropBL, cropBox.Top + cropBox.Height); Canvas.SetLeft(cropBR, cropBox.Left + cropBox.Width); Canvas.SetTop(cropBR, cropBox.Top + cropBox.Height); UpdateMask(); cropping = true; } |
The UpdateCropBox method updates the position of all the cropCanvas elements based upon the Left, Top, Width and Height properties of the cropBox Rect. When it's time to finally apply the cropping (by clicking the Crop button again), C1Bitmap joins in on the action as we grab the pixels within the bounding Rect and copy them to a new C1Bitmap, replacing the original.
C# |
Copy Code
|
---|---|
private void CropImage() { bitmap2 = new C1Bitmap((int)cropBox.Width, (int)cropBox.Height); bitmap2.BeginUpdate(); for (int x = 0; x < cropBox.Width; ++x) { for (int y = 0; y < cropBox.Height; ++y) { bitmap2.SetPixel(x, y, bitmap.GetPixel(x + (int)cropBox.X, y + (int)cropBox.Y)); } } bitmap2.EndUpdate(); bitmap.Copy(bitmap2, false); UpdateImage(true); InitCropHandles(); } |