Skip to main content Skip to footer

Working with Silverlight 4 to Build an Image Editor

With the official release of Silverlight 4, many familiar features are now possible out of the box. These features include drag and drop management, handling the right mouse button click, and printing. While most of these features were already possible using ComponentOne Silverlight 3 controls, Silverlight 4 adds value and flexibility to our studio as we continue to push the limits of the framework itself. In this sample we will combine some of the new Silverlight 4 functionality with some classic ComponentOne Silverlight controls to enhance a common type of application; an image editor. Back in the Silverlight 2.0 days, ComponentOne released C1Bitmap, a fully manageable bitmap API for working with images on the client. In this sample we will pull together some different features of C1Bitmap, along with new features of Silverlight 4 to build a full image editing application. The features of Silverlight 4 we will take advantage of are:

  • Drag and Drop onto Silverlight from outside sources
  • Printing

Users will be able to drag an image from an outside source, such as their Documents folder and drop it into the Silverlight Image Editor. Once in the editor, the user will then be able to print the image. Functionalities we will implement using C1Bitmap are:

  • Cropping
  • Resizing
  • Warping
  • Undo/Redo

We will also be using C1Toolbar from Studio for Silverlight to complete the application.

Setting up the Toolbar

Our application will consist of a C1Toolbar across the top, and a checkered background filling the remaining region of the application. We are using the C1Toolbar with the C1ToolbarStrip to create the toolbar, with Ribbon-like groups. ComponentOne Studio for Silverlight includes a convenient checkered panel control, C1CheckeredBorder, which is included in the C1.Silverlight.Extended library. This helps with the design of the image editor. Our toolbar consists of 3 C1ToolbarGroups: File, Edit and Image.


<c1tb:C1Toolbar Name="c1Toolbar1" Grid.Row="0">  
     <c1tb:C1ToolbarGroup Header="File">  
         <c1tb:C1ToolbarStrip >  
             <c1tb:C1ToolbarButton Name="btnSave" Click="btnSave_Click">  
                 <Image Source="Resources/save.png" Margin="2" ToolTipService.ToolTip="Save"/>  
             </c1tb:C1ToolbarButton>  
             <c1tb:C1ToolbarButton Name="btnOpen" Click="btnOpen_Click">  
                 <Image Source="Resources/Open.png" Margin="2" ToolTipService.ToolTip="Open"/>  
             </c1tb:C1ToolbarButton>  
             <c1tb:C1ToolbarButton Name="btnPrint" Click="btnPrint_Click">  
                 <Image Source="Resources/Print.png" Margin="2" ToolTipService.ToolTip="Print"/>  
             </c1tb:C1ToolbarButton>  
         </c1tb:C1ToolbarStrip>  
     </c1tb:C1ToolbarGroup>  
...  
</c1tb:C1Toolbar>  

Adding Drag-and-Drop Behavior

Silverlight 4 gives us inherent drag and drop events on all controls (DragOver, DragLeave, Drop, etc). This enables us to manage a drag-and-drop process among virtually any elements. But more importantly, Silverlight 4 enables us to drag items from outside the Silverlight application. All you have to do is set the AllowDrop property for your container to True, and then handle the Drop event to grab the files and do what you please. In this sample we will be taking some code from a sample posted by Microsoft. We will use the C1CheckeredBorder control as our drop-enabled control.

<c1ext:C1CheckeredBorder Name="checkeredBack" AllowDrop="True" Drop="DropTarget_Drop" />

private void DropTarget_Drop(object sender, DragEventArgs e) { // Get FileInfo array from DragEventArgs IDataObject dataObject = e.Data; var files = (FileInfo[])dataObject.GetData(DataFormats.FileDrop);

//Grab first file
if (files != null)
{
    FileInfo file = files[0];
    if (IsImageFile(file.Extension))
    {
        Stream stream = file.OpenRead();
        LoadImageStream(stream);
    }
}

}

Notice when you drag an image file from your computer onto the Web browser, you see the cursor change to signify a drop. Now, this sample is designed to only accept one file (image extension check is performed), but you can easily drop multiple files at once, as Microsoft's sample demonstrates.

Opening and Saving the Image

Two vital operations in our application are the open and save actions. We have configured the open image method to work for both when the user drops an image file onto the application and if the user decides to browse their machine. We accomplish this by passing a simple stream as our input, and we configure the application to load an image from the stream. We use baked-in OpenFileDialog and SaveFileDialogs to give our application access to files on the users machine. Once an image is loaded, we display it in a standard Image control on the page, but behind the scenes we are loading this into a C1Bitmap component. From there we will be able to further manipulate the image with actions such as cropping, resizing and warping.

Printing the Image

Once the image is loaded we can easily print. Silverlight 4's printing capabilities add plenty of value to almost any Silverlight application. The printing features basically include a PrintDocument component. We will be modeling our printing code after the same sample from Microsoft used for drag-and-drop. To print we simply call the PrintDocument.Print method, and in the PrintPage event we set the document's PageVisual property to any UI Element we want, in this case it's our image container. We could, if we wanted, print the entire toolbar with the image too but that's just odd.

PrintDocument printDocument = new PrintDocument(); private void btnPrint_Click(object sender, RoutedEventArgs e) { printDocument.Print("My Image"); }

void printDocument_PrintPage(object sender, PrintPageEventArgs e) { e.PageVisual = imageGrid; e.HasMorePages = false; }

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.

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).

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  e.HorizontalChange)
{
    cropBox = new Rect(left, cropBox.Top, cropBox.Width - e.HorizontalChange, cropBox.Height);
}
if (top > 0 && top  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  cropBox.Left)
{
    cropBox = new Rect(cropBox.Left, cropBox.Top, left - cropBox.Left, cropBox.Height);
}
if (top > 0 && top  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  e.HorizontalChange)
{
    cropBox = new Rect(left, cropBox.Top, cropBox.Width - e.HorizontalChange, cropBox.Height);
}
if (top > 0 && 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  cropBox.Left)
{
    cropBox = new Rect(cropBox.Left, cropBox.Top, left - cropBox.Left, cropBox.Height);
}
if (top > 0 && top  0)
{
    cropBox = new Rect(cropBox.Left, cropBox.Top, cropBox.Width, cropBox.Height   e.VerticalChange);
}
UpdateCropBox();

}

We apply some logic for the bounds of each adorner 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.

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.

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

Resizing the Image

The 2nd most useful image editing task is resizing, or scaling, an image. C1Bitmap provides us with easy ways to resize the image. Most of the work in this part is actually just creating the UI to capture the input from the user. In this sample, we display a Child Window prompting for a new Width and Height for the image. Then we pass these values to our resizing method which will do the work and replace the existing image with the new size. Ideally, we will want to constrain proportions on the resize action but also allow the user to resize freely. This can be handled entirely through the UI and not rely on the bitmap component.

void ResizeImage(int w, int h) { bitmap = new C1Bitmap(bitmap, w, h); UpdateImage(true); }

Undo and Redo History

We all make mistakes. Having the ability to undo (and then possibly redo) mistakes in any application is extremely convenient because saves us all time from having to start over. In this sample we take a straightforward approach to enabling Undo/Redo by saving up to 3 copies of the image after each change is applied. The changes include cropping, resizing and warping. The trick is knowing how to traverse back and forth through the history, while also enabling the user to tack on more additional changes. The code for this really has nothing to do with C1Bitmap or Silverlight 4 enhancements, so it can be used to undo/redo any particular control.

List undoBitmaps = new List();List redoBitmaps = new List();

private void btnUndo_Click(object sender, RoutedEventArgs e) { if (undoBitmaps.Count > 1) { bitmap = new C1Bitmap(undoBitmaps.ElementAt(undoBitmaps.Count - 2)); redoBitmaps.Add(new C1Bitmap(undoBitmaps.ElementAt(undoBitmaps.Count - 1))); undoBitmaps.RemoveAt(undoBitmaps.Count - 1); UpdateImage(false); } UpdateEditButtons(); }

private void btnRedo_Click(object sender, RoutedEventArgs e) { if (redoBitmaps.Count > 0) { bitmap = new C1Bitmap(redoBitmaps.ElementAt(redoBitmaps.Count - 1)); undoBitmaps.Add(new C1Bitmap(redoBitmaps.ElementAt(redoBitmaps.Count - 1))); redoBitmaps.RemoveAt(redoBitmaps.Count - 1); UpdateImage(false); } UpdateEditButtons(); }

void UpdateHistory() { //Add current bitmap to memory undoBitmaps.Add(new C1Bitmap(bitmap)); redoBitmaps.Clear();

//Restrict application to only hold up to 3 instances or changes made to C1Bitmap for undo/redo history
if (undoBitmaps.Count > 4)
    undoBitmaps.RemoveAt(0);

UpdateEditButtons();

}

Warping - Just for Fun

The initial demo for the C1Bitmap component showed how to manipulate an image pixel by pixel through warping. This can be seen today in the Control Explorer. The code basically uses a lot of math to apply circular transforms throughout the images pixels. For this sample I have made no changes to this code- I just wanted to add it for fun.

Download Full Sample

Some of the code is omitted from this blog post for brevity. All of the code can be found in the sample, and it's free to download and use. The sample targets Silverlight 4 so Visual Studio 2010 is a requirement. You must also download ComponentOne Studio for Silverlight 4 to get the C1Toolbar, C1CheckeredBorder and C1Bitmap components used in the sample.

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus