Click here to Skip to main content
15,879,326 members
Articles / Desktop Programming / WPF

How to Add Simple Photo Processing to Your WPF Application

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
24 May 2023Public Domain9 min read 10.8K   70   6  
Let the user import, adjust and view images
WPF makes it easy to include image files into the UI. However, giving the user the ability to import .jpg and similar files, choosing which part of the photo should get displayed and displaying it as thumbnail EFFICIENTLY is a challenge. This article does not show a complete application, but the problems one encounters and how to solve them.

Introduction

This is a continuation of my previous article, How to Make WPF Behave like Windows when Dealing with Images (Solving DPI Problems), which explains how to prevent WPF displaying images in different sizes than Windows. In this article, I describe all other functionality needed so that the user can import and display pictures.

Importing Images from Clipboard

There are different possibilities from where the user might want to import an image:

  • He chooses a picture file in Windows Explorer and copies it into the Clipboard
  • He copies the path to a picture file into the Clipboard
  • He makes a screen grab which gets stored in the Clipboard

Once the image is in the Clipboard, he uses Ctrl + v to paste the image into the WPF application.

To catch Ctrl + v, add a KeyDown event handler to the Window where the user can import images:

C#
private void Window_KeyDown(object sender, KeyEventArgs e) {
  if (e.KeyboardDevice.Modifiers == ModifierKeys.Control) {
    if (e.Key == Key.V) {
      pasteFromClipboard();
    }
  }
}

More challenging is reading the Clipboard. In theory, it should be simple, something like:

C#
if (Clipboard.ContainsText()) {
  var filePath = Clipboard.GetText();
  //check if the filePath really is an image file path and read the file
} else if (Clipboard.ContainsImage()) {
  var image = System.Windows.Clipboard.GetImage();
  //process the image
}

After a lot of trial and error, I found that Clipboard.ContainsText() doesn't work as expected and I had to change the code to this:

C#
BitmapSource? bitmapSource;

if (Clipboard.ContainsImage()) {
  bitmapSource = Clipboard.GetImage();
} else {
  var dataObject = Clipboard.GetDataObject();
  var formats = dataObject.GetFormats();
  if (formats.Contains("FileName")) {
    var f = dataObject.GetData("FileName");
    var fn = ((string[])Clipboard.GetData("FileName"))[0];
    var extension = System.IO.Path.GetExtension(fn)[1..].ToLowerInvariant();
    //WPF Imaging includes a codec for BMP, JPEG, PNG, TIFF, Windows Media Photo, 
    //GIF, and ICON image formats
    if (extension == "jpg" || extension == "png" || extension == "gif" || 
      extension == "bmp" || extension == "tiff" || extension == "icon") 
    {
      bitmapSource = new BitmapImage(new Uri(fn));
    } 
  }
}

BitmapSource is the base class containing a hidden bitmap which stores for each pixel of the image its value. The derived classes help with creating a BitmapSource or processing it.

Before the image can get displayed to the user, one has to undo the DIP (Device Independent Pixel) WPF uses.

Windows does not use the DPI (Dots Per Inch) information of the image file. The size of the image displayed just depends on how many pixels the image contains:

WPF on the other hand shows images with the same number of pixels but different DPIs with different sizes:

To avoid confusing the user, one has to undo WPF's DPI handling before displaying the picture to the user. This can be done like this:

C#
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.UriSource = new Uri(fileName);
bitmapImage.EndInit();

BitmapSource bitmapSource;
var dpi = VisualTreeHelper.GetDpi(this);
if (bitmapImage.DpiX==dpi.PixelsPerInchX && bitmapImage.DpiY==dpi.PixelsPerInchY) {
  //use the BitmapImage as it is
  bitmapSource = bitmapImage;
} else {
  //create a new BitmapSource and use dpi to set its DPI without changing any pixels
  PixelFormat pf = PixelFormats.Bgr32;
  int rawStride = (bitmapImage.PixelWidth * pf.BitsPerPixel + 7) / 8;
  byte[] rawImage = new byte[rawStride * bitmapImage.PixelHeight];
  bitmapImage.CopyPixels(rawImage, rawStride, 0);
  bitmapSource = BitmapSource.Create(bitmapImage.PixelWidth, bitmapImage.PixelHeight,
  dpi.PixelsPerInchX, dpi.PixelsPerInchY, pf, null, rawImage, rawStride);
}
ImageControl.Source = bitmapSource;

For a detailed explanation how this works, please see my article titled How to Make WPF Behave like Windows when Dealing with Images (Solving DPI Problems).

Note

It is important to use BeginInit() to construct bitmapImage. There is also a constructor BitmapImage(string URL). It is simpler to use, but gives a headache later when the user decides to delete the file. That produces the error message File.Delete("imageFilePath") cannot access the file, because another process is accessing it. The "other" process is actually our application, because BitmapImage(string URL) doesn't release the file until bitmapSource gets garbage collected. There is not even a Dispose() for BitmapImage. How crazy is that?

bitmapSource just stores the pixel values of the image. ImageControl is a FrameworkElement and can be used to display the content of bitmapSource.

Letting the User Chose Which Part of the Image He Would Like to Import

The user might want to import only part of the image and make it bigger or smaller. My WPF application Photo Importer is doing just that:

First, the user selects in a Windows application like Windows Explorer a picture, copies it to the clipboard, then switches into Photo Importer and presses Ctrl + v. The user can zoom in and out with the Zoom Scrollbar on the right. With the mouse, he can move the gray Selection Rectangle. The area inside the rectangle is the part of the image that will be stored in the application.

In the footer are also TextBoxes where the user can key in X and Y offset manually instead of using the mouse. He can also enter Width and Height of the gray Selection Rectangle.

By the way, this is a tricky part when programming that app. The user thinks in pixels of the image, while WPF uses DIP for X, Y , Width and Height. Depending on the resolution (DPI) of the monitor, 1 image pixel might cover several monitor pixels or only part of a monitor pixel.

This line reads the monitor's DPI:

C#
var dpi = VisualTreeHelper.GetDpi(this);

To translate from image pixels (WidthTextBox) to WPF DIP (SelectionRectangle.Width):

C#
SelectionRectangle.Width = int.Parse(WidthTextBox.Text)/dpi.DpiScaleX;

To translate from WPF DIP (mouse movement) to image pixels (position Selection Rectangle):

C#
private void SelectionRectangle_MouseMove(object sender, MouseEventArgs e) {
  if (e.LeftButton==MouseButtonState.Released) return;

  var newMousePosition = e.GetPosition(ImageGrid);
  newMousePosition.Offset((selectionPositionStartX - mouseStartPosition.X),
    (selectionPositionStartY - mouseStartPosition.Y));
  setSelectionPosition
    (newMousePosition.X*dpi.DpiScaleX, newMousePosition.Y*dpi.DpiScaleY);
}

The above code might be difficult to understand. Here is an explanation in pseudo code.

First, we calculate how many DIPs the mouse has travelled:

C#
var mouseTravelDistance = newMousePosition - mouseStartPosition;

We then add this difference to the original SelectionRectangle position:

C#
var newSelectionRectanglePosition = selectionPositionStart + mouseTravelDistance

Finally, we multiply newSelectionRectanglePosition with dpi.DpiScaleX to get the selection position in number of image pixels.

In theory, the change of mouse position in DIP could be used directly to calculate the new position of the SelectionRectangle, which is also in DIP (i.e., X and Y). But since we don't want to allow the user to move the SelectionRectangle outside the image, we have to limit the SelectionRectangle position (DIP) with the maximal dimension of the image (pixel). Which means

  1. calculate how many DIPs the mouse has moved
  2. translate that DIP distance into a number of pixels
  3. calculate the new SelectionRectangle position in pixel (newPosition = oldPosition + MouseMovementInPixel)
  4. limit the new SelectionRectangle position to the max dimension of the image
  5. convert the limited new SelectionRectangle position back into DIPs and use that value to position the Selection Rectangle.

Actually, it is even a bit more complicated, for details, see the code attached to this article.

Luckily, zooming in and out can easily be implemented using a LayoutTransform:

C#
ImageControl.LayoutTransform = new ScaleTransform(zoomFactor, zoomFactor);

A zoomFactor of 1 does not change anything, 0.5 decreases the displayed image by half, 2 doubles the image in size.

Note

The value scale of the zoom Scrollbar must be logarithmic/exponential. Let's say if the user moves the Scrollbar by an inch and the image size doubles, i.e., zoomFactor becomes 2. If the user moves the Scrollbar by another inch, the image size should double again and zoomFactor becomes 4, not 2. Again, for details, see the attached code.

Storing the New Image

At this point, the user has chosen which part of the image he wants to use. That is all the functionality in Photo Importer. How the image gets saved is very application specific, for example, in a harddisk directory or in a database. Also, how an image gets linked to the other data is application specific.

I have attached the code for the class ImageCache. It stores the images in RAM and in a Windows directory. Even WPF is astonishingly fast displaying images stored in a SSD drive, I felt it would be better to read the pictures in a cache first and then display it, because in my application, the same image gets displayed several times. Furthermore, I wanted a small thumbnail for every image which I then could use to display in a DataGrid.

I would have liked to have the ImageCache not in the UI layer, but in a lower layer which has no reference to WPF. Unfortunately, a BitmapSource must be used to store an image for WPF in RAM, so I placed my ImageCache in the UI layer. Of course, I could place ImageCache also in its own DLL, which would have the advantage that it is easier to write unit tests for it.

I found it best to store all images in two different Dictionary<int, BitmapSource>, one for normal sized pics and one for thumbnails, which I use quite often when I display my data in a DataGrid. The int is the UserId, which uniquely identifies each user and will never change. The image I store in a SSD directory with the filename being Pic999.jpg, where 999 stands for the actual UserId.

Note

I expect my application to store not more than 1000 pictures and I don't mind using 1GByte of RAM for that purpose. If your application deals with tons of pictures, you could write a more sophisticated cash which removes unused pics from the cache or not use a cache at all, since reading an image file from a SSD drive is surprisingly fast. Even if you don't use ImageCache, use its code to guide you when you write reading, storing and deleting images.

Displaying the Image

Displaying an image in the size it is stored is straight forward:

XML
<Border HorizontalAlignment="Center" VerticalAlignment="Center" 
        Margin="5" BorderBrush="Black" BorderThickness="2">
  <Image x:Name="ImageControl" Stretch="None"/>
</Border>

In code behind:

C#
ImageControl.Source = bitmapSource;

Displaying the thumbnail version in a DataGrid goes like this:

XML
<DataGridTemplateColumn Header="Pic" Width="SizeToHeader">
  <DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
      <Image Source="{Binding Pic}"  Margin="-1"/>
    </DataTemplate>
  </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

The DataGrid.DataContext binds to a list of user records, which has a property with the name Pic and the type BitmapSource. The thumbnail image gets created like this:

C#
const double maxWidth = 19;
const double maxHeight = 30;
var scaleFactor = Math.Min(maxWidth / bitmapSource.Width, 
                  maxHeight / bitmapSource.Height);
userRecord.Pic =
  new TransformedBitmap(bitmapSource, new ScaleTransform(scaleFactor, scaleFactor));

I know, reading this article is tedious. But I wish someone else would have written it when I started my application. I hope it will be helpful for others.

Recommended Reading

If you are interested in WPF, I strongly recommend looking at some of my other WPF articles on CodeProject, which are more enjoyable to read:

My most useful WPF article:

The WPF article I am the proudest of:

Indispensable testing tool for WPF controls:

WPF information sorely lacking in MS documentation:

I also wrote some non WPF articles.

Achieved the impossible:

Most popular (3 million views, 37'000 downloads):

The most fun:

I wrote MasterGrab 6 years ago and since then, I play it nearly every day before I start programming. It takes about 10 minutes to beat 3 robots who try to grab all 200 countries on a random map. The game finishes once one player owns all the countries. The game is fun and fresh every day because the map looks completely different each time. The robots bring some dynamics into the game, they compete against each other as much as against the human player. If you like, you can even write your own robot, the game is open source. I wrote my robot in about two weeks (the whole game took a year), but I am surprised how hard it is to beat the robots. When playing against them, one has to develop a strategy so that the robots attack each other instead of you. I will write a CodeProject article about it sooner or later, but you can already download and play it. There is good help in the application explaining how to play:

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer (Senior)
Singapore Singapore
Retired SW Developer from Switzerland living in Singapore

Interested in WPF projects.

Comments and Discussions

 
-- There are no messages in this forum --