WPF Image Processing Control
A user control that allows pasting images from clipboard, cropping and transforms.
Introduction
If you have not yet tried to paste an image from the clipboard to a WPF image control, you are in for a bit of a bit of a surprise. It doesn’t work! When I encountered the problem, I first thought that I probably just need to be tutored by some of the experts on how it’s done. Microsoft gave us such wonderful tools in WPF to work with images with so much ease; it comes as a shock that this is not as easy as expected! For me, this discovery set me off into the deepest quagmire I’ve been in for a while…..ah but I’m getting ahead of myself.
Once I understood that it was a problem, I started searching for WPF Image manipulation articles. One of the best is WPF Interactive Image Cropping Control by Sacha Barber. Click here.
If you read his article in detail, you can almost feel his pain while struggling with advanced imaging in WPF. (He finally resorted to using some old GDI+ DLLs) The bottom line is that while Microsoft gave us some really nice high level tools in the System.Windows.Media.Imaging
Namespace. Click here. But, when you need to go beyond basic loading and saving images (like pasting from a clipboard for example) these marvelous tools kind of leave you without a parachute. One alternative is to go back to GDI+, so the question is can it be done in Framework 4.0?
Background
Barber’s article sparked my imagination: "What would I really like to have in an image processing control?" I often need to work with images in a uniform size so it would be nice to be able to scale and crop an image. It would be really great, if we did not need to pop out to our favorite photo processing program; I want to do these things right in WPF! Sometimes, the person’s picture faces left and I need an image that faces right, so let’s add some basic rotate and flip transforms to our requirement. I often need to give credit to the source of an image so our new image control should be friendly to some of the stuff that’s already buried in the image. I’d like to read and write metadata. We should be able to load from a file and save our image to a file. And last, this all started with the need to grab an image from the clipboard. So there you have it, the basic requirements for a SmartImage Control.
How To Use It
So here it is, it' a complete user control, let’s give this new SmartImage Control a test drive.
The easy way is to download the solution (above) which has a Tester Project included. Or if you want to try the control in your own project, all you need to do is add the SmartImage
DLL (above)and include it in the references in your project. Be sure that you have auto sized the region that will contain an instance of the Control because it’s going to need some serious elbow room.
In the Test project, the SmartImage
starts with a target size of 200 x 150, but when you paste an image, it may take up the entire screen. I guess I just introduced the notion of a target. The control has several states, Target
(it's beginning and result condition, DragCanvas
, where you can move things, and the SelectionCanvas
, where you can resize clip selector. We start out with a target area that is the size of our desired image result. There are two ways to fill the target. If you right click, you can choose the Dashboard
, where you can load an image from a file location, or you can choose Paste.
![]() | Let’s go with Paste.
So stop right there and open your browser and copy a really big image to the clipboard, you can usually find large images at a sports sites, or try something like "Towers of London" in Google. Once you have a nice big image on the clipboard, try and paste it on our little 200 x 150 Gray Target. Wow! Full Page, eh ! |
Oh, before I go on, our Target was gray, but it’s not just a fill color of Gray. Here’s a little tip that can save you hours of head scratching. An empty image control will not recognize a click event! This Target image is pre-loaded with a gray image (look in the project Resources folder), you could change the image to your favorite background or perhaps instructions for the user, but just remember the target has to be preloaded with an image or you can forget about a ‘right click to paste’.
OK, you are now looking at your pasted image on a Drag Canvas (thanks to Sacha Barber & Josh Smith). You also see that there is a patch of yellow, which is exactly the same size as our original target. I guess you know that this patch is generally called a "Rubber Band".
Well it’s on a Drag Canvas, so go ahead and drag the rubber band around, with the left mouse button, until it covers the area of the image that you would like to capture in your target. A right click of the mouse will give you a context menu where you can capture the cropped portion of the image (the area that lies beneath the rubber band). So select "Capture" and you have exactly the right part of the pasted image nicely fitted into the Target space. And the Dashboard Menu is revealed where you can add some picture credits. Depending on which of the examples you chose to copy to the clipboard type something like, "Fox Sports" or "Queen of England" or "Microsoft" in the text box at the top of the Dashboard Menu, and click the button ‘Save MetaData’. You will then notice that a credit line is displayed below the target. This is actually showing you a part of the metadata that is now contained in our new image. (More about this later.)
![]() | We are almost home, you can store this new resized image by using the "Save Image to File" button. Phew. We made it through our test drive without a crash. We skipped lots of details but most of the transforms are pretty intuitive. One interesting point is that if you do a capture and after you take a look at the results, you decide it would be best to have another go at it . . . just ‘right click and paste’ again. This puts you right back in the Drag Canvas. |
While you are in the Drag Canvas, a right click will allow you to "Reset Crop Region". The yellow rubberband patch that matched the Target is gone, but you are on Selection Canvas where you can select the size of your own Rubberband with a left click and a drag. Keep in mind that the resulting crop may not match the Target.
My own view is that when working in graphics, left and right clicking are quite intuitive with a mouse, but a bit awkward if you are using a touch panel on a lap top. If you are new to graphics, just hang in there, using the right mouse button will become second nature.
As I mentioned before, if you are still using training wheels with System.Windows.Media.Imaging
, it can be a real quagmire. My own experience was even more frustrating because I often could not tell if I was looking at a GDI+ term or a new WPF media term. It is very difficult to distinguish between the two (for example, Bitmap
vs BitmapImage
), but be warned . . . they don’t mix nearly as well as oil and water. Another, issue that contributed to hours of dead end’s for me is ‘file locking’. System.Windows.Media.Imaging
seems to throw on a file lock at the most inconvenient time. Laborius hours of patient coding has to be dumped in the hopper because there is just no way to avoid a locked file. In this SmartImage
project, I use three named files (ClipSourceTemp.jpg, ClipResultTemp.jpg and ClipTextTemp.jpg) as buffers to manipulate the images and work around the file locking.
How it works
Here are the highlights of the design. The basic concept is to place an instance of
the SmartImageUserControl
in your project and size the gray image. It has two Dependency Properties called TargetWidth
and TargetHeight
. As shown here.
Public Property TargetHeight() As Double
Get
Return DirectCast(GetValue(TargetHeightProperty), Double)
End Get
Set(value As Double)
SetValue(TargetHeightProperty, value)
End Set
End Property
Public Shared TargetHeightProperty As DependencyProperty = _
DependencyProperty.Register("TargetHeight", GetType(Double),
GetType(SmartImageUserControl),
New UIPropertyMetadata(150.0, _
New PropertyChangedCallback(AddressOf OnTargetHeightPropertyChanged)))
Private Shared Sub OnTargetHeightPropertyChanged(ByVal d As DependencyObject, args As DependencyPropertyChangedEventArgs)
Dim uc As SmartImageUserControl = DirectCast(d, SmartImageUserControl)
uc.TargetImage.Height = args.NewValue
End Sub
You don’t have to set the TargetWidth
/TargetHeight
properties
because if you look carefully at the Dependency Property metadata, you will see
that a default value is given.
When you select the paste option you may find it interesting
that we do not actually paste directly to the image control source (the image
control that serves as the base for SmartImage
). In fact the clipboard converter returns an
Image source object, which is a bitmapFrame
, rather than a bitmapImage
. In
short, it’s the wrong type and doesn’t lend itself to type conversion. So what to do? Since I already found that we needed to do
some buffering to avoid file locking, I simply saved the clipboard converter’s
output directly into one of the file buffers "ClipSourceTemp.jpg" using ‘SaveToJpg
’ located in our handy
ImageTools library. Next, the ImageCropper’s
source property is loaded from "ClipSourceTemp.jpg". Keep
in mind that this file is now locked.
Most of the ImageCropper
, SelectionScreen
, and Drag screen are
direct lifts from Barbers, work, but have been expanded to include more Zooms
and some specific Rotate and Flip Transforms.
It will help if you have a basic understanding of the design. Click
Here
Let’s take a closer look at the Image menu where the user
selects these effects. Since the effects
are not mutually exclusive, we need to contemplate that several effects may be
applied to the same image. My answer was
to package all of the selections in a single custom event called SelectionChanged
.
Public Delegate Sub SelectionChangedEventHandler(ByVal sender As Object, ByVal e As ImageMenuArgs)
Public Shared ReadOnly SelectionChangedEvent As RoutedEvent = _
EventManager.RegisterRoutedEvent("SelectionChanged",_
RoutingStrategy.Bubble, GetType(SelectionChangedEventHandler), GetType(ImageMenu))
Public Custom Event SelectionChanged As SelectionChangedEventHandler
AddHandler(ByVal value As SelectionChangedEventHandler)
Me.AddHandler(SelectionChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As SelectionChangedEventHandler)
Me.RemoveHandler(SelectionChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As ImageMenuArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
This event paired with its Custom Arguments, shown below deliver everything we need to the Image Cropper to facilitate applying the user choices from the menu.
Public Class ImageMenuArgs
Inherits RoutedEventArgs
#Region "Declariations"
Private m_ZoomFactor As Double = 1.0
Private m_Rotation As Double = 0.0
Private m_IsFlipHorizontal As Boolean = False
Private m_IsFlipVertical As Boolean = False
Private m_IsResetClipToTarget As Boolean = True
#End Region
#Region "Properties"
Public ReadOnly Property ZoomFactor() As Double
Get
Return m_ZoomFactor
End Get
End Property
Public ReadOnly Property Rotation() As Double
Get
Return m_Rotation
End Get
End Property
Public ReadOnly Property IsFlipHorizontal() As Boolean
Get
Return m_IsFlipHorizontal
End Get
End Property
Public ReadOnly Property IsFlipVertical() As Boolean
Get
Return m_IsFlipVertical
End Get
End Property
Public ReadOnly Property IsResetClipToTarget() As Boolean
Get
Return m_IsResetClipToTarget
End Get
End Property
#End Region
Public Sub New(ByVal routedEvent As System.Windows.RoutedEvent, _
ByVal ZoomFactor As Double, ByVal Rotation As Double, _
ByVal IsFlipHorizontal As Boolean, _
ByVal IsFlipVertical As Boolean, IsResetClipToTarget As Boolean)
MyBase.New(routedEvent)
m_ZoomFactor = ZoomFactor
m_Rotation = Rotation
m_IsFlipHorizontal = IsFlipHorizontal
m_IsFlipVertical = IsFlipVertical
m_IsResetClipToTarget = IsResetClipToTarget
End Sub
End Class
Once the expanded Image Cropper, knows what to do by receiving this
event, it proceeds manipulating the image, drawing heavily upon the library in
the ImageTools
class.
The use of a pop-up context menu in the SmartImageUserControl
proves very useful and is quite intuitive for the user.
Here is the code for the ContextMenu
.
Private Sub createMainPopUpMenu()
cmMain = New ContextMenu()
Dim miDashboard As New MenuItem()
miDashboard.Header = "Dashboard"
Dim miCancel As New MenuItem()
miCancel.Header = "Cancel"
Dim miSave As New MenuItem()
miSave.Header = "Paste"
cmMain.Items.Add(miCancel)
cmMain.Items.Add(miDashboard)
cmMain.Items.Add(miSave)
cmMainRoutedEventHandler = New RoutedEventHandler(AddressOf MainPopUpOnClick)
cmMain.[AddHandler](MenuItem.ClickEvent, cmMainRoutedEventHandler)
Me.ContextMenu = cmMain
End Sub
ContextMenu
results are handled like this.
Private Sub MainPopUpOnClick(sender As Object, args As RoutedEventArgs)
Dim item As MenuItem = TryCast(args.Source, MenuItem)
Select Case item.Header.ToString()
Case "Dashboard"
Dashboard.Visibility = Windows.Visibility.Visible
Exit Select
Case "Paste"
CreateSourceFileFromClipboard()
Exit Select
Case "Cancel"
Exit Select
Case Else
Exit Select
End Select
End Sub
Notice that while the user is selecting Rotates, Flips, and Zooms, the image is transformed concurrent with the selections, so that when the user finally selects the capture button all that remains to be done is to apply the clipping required to grap the part of the image that lies inside the rubberband.
Private Sub SaveCroppedImage()
Dim ImgTools As New ImageTools
Dim hScale As Single = 1.0F / CSng(zoomFactor)
Dim vScale As Single = 1.0F / CSng(zoomFactor)
rubberBandLeft = Canvas.GetLeft(rubberBand)
rubberBandTop = Canvas.GetTop(rubberBand)
ImgTools.CropImageToFile(bmpSource, CSng(rubberBandLeft) * hScale, _
CSng(rubberBandTop) * vScale, CSng(rubberBand.Width) * hScale, _
CSng(rubberBand.Height) * vScale)
createDragCanvas()
Dim args As New RoutedEventArgs(CropResultEvent)
MyBase.RaiseEvent(args)
End Sub
This code simply gets the size and location of the rubberband and sends it all with the image(bmpSource) to the ImageTools libray, where the clipping is done and the results are hard wired to a save at the second buffer location, called "ClipResultTemp.jpg".
All that remains is to play with the Exif metadata, The image results
are in ClipResultTemp.jpg so if you look at this piece of code in the Image
Dashboard, you will see that all we do is load the contents of the results
file, modify the metadata and put the result back in the same file. An event (MetaDataChanged
) is raised which is handled in the
SmartImageUserControl
where once more the ClipResultTemp
is examined and the
TextBlock displays the change. The Exif
routines also encounter the file locking issue so another fixed location ClipTextTemp.jpg is used as a work around.
This code provides a nice model for other occasions where you
want to display an image along with its mteatdata, and it’s all you need for
both the image and the Exif data. (of course along with the ExifTools
class). Be sure to uncomment the code below to see
data examples in message Boxes.
Private Sub SaveMetaData_Click(sender As Object, _
e As System.Windows.RoutedEventArgs) Handles SaveMetaData.Click
Try
Dim imgUrl As String
Dim f As New FileInfo("ClipResultTemp.jpg")
imgUrl = f.FullName
Dim ExTools As New ExifTools(imgUrl)
'Un-comment the 1st MessageBox to see
'all of the fields current available
'in the ExifTools prior to edit.
'MessageBox.Show(ExTools.ToString())
ExTools.Copyright = MetaDataText.Text
'Un-comment the 2nd MessageBox to see
'all of the fields current available
'in the ExifTools after the edit is applied.
'MessageBox.Show(ExTools.ToString())
f = New FileInfo("ClipTextTemp.jpg")
Dim TempImgUrl = f.FullName
ExTools.Save(TempImgUrl)
'Show sample of MetaData
'ImageTextBlock.Text = "Image courtesy of " + ExTools.Copyright.ToString()
ExTools.Dispose()
File.Copy(TempImgUrl, imgUrl, True)
Dim args As New RoutedEventArgs(MetaDataChangedEvent)
MyBase.RaiseEvent(args)
Catch ex As Exception
MessageBox.Show(ex.Message.ToString())
End Try
End Sub
Well that’s about it, we left our results in ClipResutsTemp.jpg. The Image Dashboard allows you to load files and save this special results file to a permanent location.
All in all, this may be useful not only as a User control, but if you are unfamiliar with Dependency Properties, Routed Events and Custom Event Arguments, this may serve as an example of how to use these tools. I myself learn best by following the code in a working example.
Points Of Interest
The primary project that created the need for the development of this User Control is written in Visual Studio WPF (Windows Presentation Foundation) using an MVVM (Model View View Model) Pattern under a Caliburn Micro framework and employing IoC (Inversion of Control) containers. However, it was decided that this control would benefit from as much simplicity as possible, so none of these techniques were used here in this user control and (egad!) you will find much of the code in codebehind partial classes.
C# advocates may be appalled that this is written in VB, no need to worry, You can use it as-is, just grab the DLL and it will work fine in your C# project. If you plan to make changes, just dump the whole project in a VB To C# converter. Click here. Let it grind, and viola! You have code that may be easier for you to read. I did not use any lambdas or other cool things that C# converters tend to choke on. Namespaces are manually derived, which should also help in the conversion. So hopefully your conversion will go without incident. I might add that while I have struggled so many times with C# to VB conversion and the many limitations, I have actually never done it backwards.
In VB - namespaces are normally handled automatically, so if you are a VB person and want to extend the source, be sure to pay attention to the Namespaces in this project. Namespace is set to manual. (They are set up as though the ‘SmartImage
’ Project stands on its own. (and as such, is not part of the ‘SmartImageProj
’ solution, so that the C# conversion won’t produce code that will be hard to debug).
If you plan to make the control extensible you may want to note an interesting piece of code in the SmartImage
project’s Build Events. I use a Folder Labeled Lib to contain my projects special DLLs and the following code places a new copy of the DLL in the Lib after each successful build.
xcopy /Y /I "$(TargetPath)" "$(SolutionDir)$(SolutionName)\$(OutDir)"
xcopy /Y /I "$(TargetPath)" "$(SolutionDir)\Lib\"
This is real handy if your testing project is in the same solution. All you do in the testing project is be sure that the references grab the DLL from the ‘LIB’ folder. When done in this manner, Any changes you make in the source project are automatically written to the Library folder and so the test project remains up to date with each successful build of the source.
There are a several goodies worth mentioning in the project:
The Dashboard Menu has a watermark TextBox
, this clever design was posted somewhere and I am ashamed to say that I no longer remember who deserves the credit. But it is a clever design and I use it a lot in my work. You will find all you need for the Watermark Text Box in the Resource Dictionary at the top of the XAML for DashboardMenu
.
The ImageTools
Class contains some of the breakthroughs that made this user control possible for me.
A discussion about pasting from a clipboard is contained here. The VB version of his code is contained in my ImageTools
class. The most valuable tools for low level image manipulation were found at the DrWPF BLOG. I incorporated them all in this set of tools whether they were used in this project or not. Since .jpg compression is a "lossy" form of compression and files are subject to a lot of manipulation, someone may want to expand the Control to include some of the lossless compression techniques. The decoders are here in this set of ImageTools
.
Exif (Exchangeable image file format) is a metadata standard for cameras. Most of us have not really tapped this useful but arcane set of metadata. I tripped over a set of tools called ExifWorks
by Michal A. Valášek; its adapted and included here as the ExifTools
class. While we are only using one field (Copyright)from ExifTools in this control, the class includes access to many fields of the Exif standard. They are all easily accessible, and could be very useful. But there are so many more than just Copyright, imagine using the Description Field for an image that says "From the Left are……" for a nice caption below an image. There is even provision for GPS data that would indicate the point from which a photo was taken. The nice thing is that because it's metadata, you always have access to the fields contained in the image.
History
This is my first article, so I am anxious for feedback from peer review. This control is meant to be extensible; I would love to see others publish improvements and extensions of the SmartImage
basic concepts. It’s been a real learning experience for me and I hope that it is useful for you too.