Click here to Skip to main content
15,867,765 members
Articles / Desktop Programming / WPF

WPF Image Processing Control

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
12 Dec 2013CPOL15 min read 34.4K   2.4K   14   4
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.

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

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

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

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

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

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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President Atlantic Quote Systems, Inc.
United States United States
Bob is the owner and software developer of Atlantic Quote Systems, Inc.
A software company focused on building plan takeoffs and
lumber estimating for professional lumber yards.

Comments and Discussions

 
QuestionYes sir Pin
Sacha Barber11-Dec-13 23:37
Sacha Barber11-Dec-13 23:37 
Working with advanced imaging in WPF is sadly not that much fun. Kudos for your efforts here though, well done.

As someone has already stated, it would make it more of an article to see some code snippets and their explanations, would make it much nicer article.

But good work all the same
Sacha Barber
  • Microsoft Visual C# MVP 2008-2012
  • Codeproject MVP 2008-2012
Open Source Projects
Cinch SL/WPF MVVM

Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

AnswerRe: Yes sir Pin
Bob Ranck12-Dec-13 4:18
Bob Ranck12-Dec-13 4:18 
QuestionNot really an article, yet. Pin
OriginalGriff11-Dec-13 8:43
mveOriginalGriff11-Dec-13 8:43 
AnswerRe: Not really an article, yet. Pin
Bob Ranck12-Dec-13 4:22
Bob Ranck12-Dec-13 4:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.