Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Rob's Image Shrinker

  In this article, we will create an application to resize images to desired size.
Crazy World(tm) of Rob Miles

Difficulty: Easy
Time Required: 1-3 hours
Cost: Free
Software: Visual Studio Express Editions
Hardware:
Download:

Devices like Smartphones, Pocket PCs, and Sony Playstation Portables are very useful for carrying pictures around. The problem is that modern cameras take pictures that are far bigger than a mobile device requires. A large number of megapixels makes a picture great for printing or viewing on a large display, but it just makes things difficult when you use a small device. Such high-resolution picture files take a long time to load and use up lots of file space.

Many graphics programs, for example Microsoft Digital Image Pro 10, have bulk edit features, so that you can resize a whole bunch of pictures. If you have a portable media center, you can use Media Player 10 to resize the images as they are transferred. However, what you really want is a program that will resize your pictures and transfer them all at once. So I've written one, and we can explore how it works.

Getting started is easy, whether you just want to use the program or play with the code. I've supplied a user manual in case all you want to do is use the program.

Working with the Application

I've created the project in Visual C# 2005 Express edition. Open up the appropriate project file and you will be in business. If you have not got Visual Studio you can get hold of a copy for free from http://msdn.microsoft.com/vstudio/express/visualcsharp/.

The initial download is quite small, but the installation will fetch around 300 MB of code.

Once you have Visual Studio on your machine, you can open up the appropriate directory and select the solution file. In the next sections, we are going to consider each part of the program in detail. The examples as given are not exactly as in the source code, but we want to be able to concentrate on the important aspects of each part of the development.

Setting the Destination for the Images

I set the destination by using a FolderBrowserDialog. This allows the user to find a directory where the image files are to be transferred. I've set the option so that the user can also create a new directory in which to place the images:

Visual C#

outputDirDialog = new System.Windows.Forms.FolderBrowserDialog();
outputDirDialog.Description = "Select Destination";
outputDirDialog.ShowNewFolderButton = true;
outputDirDialog.ShowDialog();

Visual Basic

If outputDirDialog Is Nothing Then
outputDirDialog = New System.Windows.Forms.FolderBrowserDialog()
outputDirDialog.Description = "Select Destination"
outputDirDialog.ShowNewFolderButton = True
End If
outputDirDialog.ShowDialog()

I use the ShowDialog method so that the rest of my program pauses while the user selects the destination using this dialog:

Figure 1. Browsing for a destination

If the user opens up the My Computer tree, they can then find all the drive letters, including any for memory devices. Note that the program must also handle the situation where the user does not select a folder but clicks the "Cancel" button instead. In this situation the length of the resulting path will be 0, and so we display an appropriate message:

Visual C#

if (outputDirDialog.SelectedPath.Length == 0)
{
statusLabel.Text = "No destination selected";
return;
}

Visual Basic

If outputDirDialog.SelectedPath.Length = 0 Then
statusLabel.Text = "No destination selected"
Return
End If

We use a label at the bottom of the form to send messages to the user.

Getting the Source Images

Once we know where to put the files, we now need to select some files for transfer. The OpenFileDialog is fine for this, since it can be configured to allow the user to select multiple files. The dialog can also show thumbnails of each image, so the user can easily see which files are to be transferred.

Visual C#

sourceFilesDialog = new OpenFileDialog();
sourceFilesDialog.Multiselect = true;
sourceFilesDialog.Title = "Select files to shrink";

Visual Basic

sourceFilesDialog = New OpenFileDialog()
sourceFilesDialog.Multiselect = True
sourceFilesDialog.Title = "Select files to shrink"

This code creates the dialog and configures it to allow multiple files to be selected. Now we can configure it to show only image files for transfer:

Visual C#

sourceFilesDialog.Filter = 
"Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|All files (*.*)|*.*";

Visual Basic

sourceFilesDialog.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|" _
& "All files (*.*)|*.*"

The filter string looks rather complicated, but is really quite simple. Elements in the string are separated by the vertical bar character. Each filter is expressed as a pair of items, a description string followed by a list of filter expressions. It looks a bit clearer if I write it as:

Visual C#

"Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|" +
"All files (*.*)|*.*"

Visual Basic

"Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF|" _
& "All files (*.*)|*.*"

The top filter lets the user select image files that are Bitmap, JPEG, or GIF images. The second line lets the user select all the files. For each line the vertical bar splits off the text to appear in front of the user from a list of file extensions that apply to that selection

When the user selects the top filter, only file names matching Bitmap, JPEG, or GIF images are shown. When the bottom filter is selected, all files are shown.

Figure 2. The filter in use

Note that just because a file has a particular extension does not necessarily mean that it contains a particular type of file; our program must make sure that an invalid file content does not cause problems. We can address this later.

Users can select as many files as they like from a directory, or even use CTRL+A to select all of them. When they click the "Open" button, the dialog completes, and the files can then be processed and transferred into the destination device.

Doing the Processing

The OpenFileDialog returns a list of file names as an array of strings. The program must now open each file, load the bitmap from it, resize this into a bitmap of the required dimensions, and then save the bitmap in the destination directory. The method processFiles is in charge of all this.

Visual C#

processFiles(sourceFilesDialog.FileNames, outputDirDialog.SelectedPath);

Visual Basic

processFiles(sourceFilesDialog.FileNames, outputDirDialog.SelectedPath)

This method is passed an array of file names and a destination path for the outputs. It must then work its way through the files and scale and save each one in turn.

Visual C#

private void processFiles(string[] FileNames, string outputPath)
{
Bitmap dest = new Bitmap(size.width, size.height);
foreach (string filename in FileNames)
{
Bitmap image;
try
{
image = new Bitmap(filename);
}
catch
{
MessageBox.Show("Error loading bitmap : " + filename);
continue;
}
scaleBitmap(dest,image);
string destFilename = outputPath + @"\" +
System.IO.Path.GetFileNameWithoutExtension(filename) + "
.jpg";
try
{
dest.Save(destFilename, System.Drawing.Imaging.ImageFormat.Jpeg);
}
catch
{
MessageBox.Show("
Error saving bitmap : " + destFilename);
return;
}
}
}

Visual Basic

Private Sub processFiles(ByVal FileNames As String(), ByVal outputPath As String)
Dim dest As Bitmap = New Bitmap(Size.Width, Size.Height)
For Each filename As String In FileNames
Dim image As Bitmap
Try
image = New Bitmap(filename)
Catch
MessageBox.Show("Error loading bitmap : " & filename, "Bitmap Load")
Continue For
End Try
scaleBitmap(dest, image)
Dim destFilename As String = outputPath & "\" _
& System.IO.Path.GetFileNameWithoutExtension(filename) & ".jpg"
Try
dest.Save(destFilename, System.Drawing.Imaging.ImageFormat.Jpeg)
Catch
MessageBox.Show("Error saving bitmap : " & destFilename, "Bitmap Save")
Return
End Try
Next
End Sub

This method works through each of the file names, creates a bitmap from each file, calls the scaleBitmap method to scale it, and then saves the result back to disk.

Note that I have put an exception handler around the creation of the bitmap from a file. If the load fails, a message is displayed and the method moves on to the next image. There is also a try catch construction around the save operation. However, if the save fails this usually means that the next save will probably fail too, because the output device might be full. For this reason, when the save call fails the method returns rather than continues.

Scaling the Bitmap

Scaling the bitmap is quite easy. There are draw methods that can be used to draw a rectangle from one image into another. By manipulating the sizes of the source and destinations, we can resize the image to fit on our device. The only difficulty is that we have to handle the aspect ratios of the source and destination, so that we don't clip off any parts of the image. This is exactly the same problem that you get when watching an old TV program on a widescreen TV or vice versa. The resize method must make the picture fit on the screen and insert empty space around it as required:

Visual C#

private Rectangle srcRect = new Rectangle();
private Rectangle destRect = new Rectangle();

private void scaleBitmap ( Bitmap dest, Bitmap src )
{
destRect.Width = dest.Width;
destRect.Height = dest.Height;
using (Graphics g = Graphics.FromImage(dest))
{
Brush b = new SolidBrush(backgroundColor);
g.FillRectangle(b, destRect);
srcRect.Width = src.Width;
srcRect.Height = src.Height;
float sourceAspect = (float)src.Width / (float)src.Height;
float destAspect = (float)dest.Width / (float)dest.Height;
if (sourceAspect > destAspect)
{
// wider than high heep the width and scale the height
destRect.Width = dest.Width;
destRect.Height = (int)((float)dest.Width / sourceAspect);
destRect.X = 0;
destRect.Y = (dest.Height - destRect.Height) / 2;
}
else
{
// higher than wide – keep the height and scale the width
destRect.Height = dest.Height;
destRect.Width = (int)((float)dest.Height * sourceAspect);
destRect.X = (dest.Width - destRect.Width) / 2;
destRect.Y = 0;
}
g.DrawImage(src, destRect, srcRect, System.Drawing.GraphicsUnit.Pixel);
}
}

Visual Basic

Private srcRect As Rectangle = New Rectangle()
Private destRect As Rectangle = New Rectangle()
Private Sub scaleBitmap(ByVal dest As Bitmap, ByVal src As Bitmap)
destRect.Width = dest.Width
destRect.Height = dest.Height
Using g As Graphics = Graphics.FromImage(dest)
Dim b As Brush = New SolidBrush(backgroundColor)
g.FillRectangle(b, destRect)
srcRect.Width = src.Width
srcRect.Height = src.Height
Dim sourceAspect As Single = src.Width / src.Height
Dim destAspect As Single = dest.Width / dest.Height
If sourceAspect > destAspect Then
' wider than high
destRect.Width = dest.Width
destRect.Height = (dest.Width / sourceAspect)
destRect.X = 0
destRect.Y = (dest.Height - destRect.Height) / 2
Else
' higher than wide
destRect.Height = dest.Height
destRect.Width = ((dest.Height * sourceAspect))
destRect.X = (dest.Width - destRect.Width) / 2
destRect.Y = 0
End If
g.DrawImage(src, destRect, srcRect, System.Drawing.GraphicsUnit.Pixel)
End Using
End Sub

Resizing images is actually quite easy. The DrawImage method can be supplied with source and destination rectangles. The method above decides which way the source needs to be scaled and then adjusts the size and position of the destination rectangle to suit. The call of DrawImage at the bottom of the method is the part that does the actual work. Note that the rectangles that we use to size the source and destination are declared outside the method. This is so that we do not end up creating and destroying new rectangles for each image that we scale. Since all we really want to do is change the dimensions of them each time we use them, there is no need to keep making new ones.

Managing the Size

The user needs to be able to select from a range of possible widths and heights, depending on the target device. The best screen component for doing this is the ComboBox, with a list of items from which the user picks one.

Figure 3. Selecting the output format

The best way to manage this is to create a special class that holds the size information. We can then supply an array of instances of this type to the ComboBox and it will let the user pick one.

Visual C#

public class OutputSize
{
public int width;
public int height;
string name;
public override string ToString()
{
return name + " " + width.ToString() + " x " + height.ToString();
}
public OutputSize(string inName, int inWidth, int inHeight)
{
name = inName;
width = inWidth;
height = inHeight;
}
}

Visual Basic

Public Class OutputSize
Public width As Integer
Public height As Integer
Private name As String
Public Overloads Overrides Function ToString() As String
Return name & " " & width.ToString() & " x " & height.ToString()
End Function
Public Sub New(ByVal inName As String, ByVal inWidth As Integer, _
ByVal inHeight As Integer)
name = inName
width = inWidth
height = inHeight
End Sub
End Class

The OutputSize type contains a name property to identify it as well as height and width values. The ToString method that it exposes provides the text shown in the ComboBox. The width and height properties are made public so that they can be used by methods in other classes. Finally, it has a constructor that lets us set up all the values. We can now create an array of instances of this class which can be used to configure the ComboBox:

Visual C#

private OutputSize[] resolutionSettings = new OutputSize[] {
new OutputSize ( "Pocket PC", 640, 480 ),
new OutputSize ( "QVGA", 320, 240 ),
new OutputSize ( "PSP", 480, 272 ),
new OutputSize ( "Smartphone", 176, 180)
};

Visual Basic

Private resolutionSettings As OutputSize() = New OutputSize() { _
New OutputSize("Pocket PC", 640, 480), _
New OutputSize("QVGA", 320, 240), _
New OutputSize("PSP", 480, 272), _
New OutputSize("Smartphone", 176, 180)}

If you want to add other sizes to the program these can just be slotted into the array and will be picked up and used automatically. Adding the settings to the ComboBox is achieved very easily:

Visual C#

resolutionComboBox.DataSource = resolutionSettings;

Visual Basic

resolutionComboBox.DataSource = resolutionSettings

This is a very powerful feature of Windows forms. The ComboBox just pulls in the settings and populates its selections with the values in the array. When we want to get the present selection, we just have to get it and cast it to the actual type that we know is in there:

Visual C#

OutputSize size = resolutionComboBox.SelectedItem as OutputSize;

Visual Basic

Dim size As OutputSize = TryCast(resolutionComboBox.SelectedItem, OutputSize)

We can now use the width and height properties of the size instance to control the scaling.

Setting the Background Color

I let the user select the background color that is used to fill around images when their aspect ratio is not quite right. This color is stored as a member of the form:

Visual C#

private Color backgroundColor = Color.White;

Visual Basic

Private backgroundColor As Color = Color.White

Initially it is set to white, but the user can select different colors according to taste. I use the ColorDialog dialog to allow the user to do this:

Visual C#

backColorDialog = new ColorDialog();
backColorDialog.SolidColorOnly = true;
backColorDialog.Color = backgroundColor;
backColorDialog.ShowDialog();

Visual Basic

If backColorDialog Is Nothing Then
backColorDialog = New ColorDialog()
backColorDialog.SolidColorOnly = True
End If
backColorDialog.Color = backgroundColor
backColorDialog.ShowDialog()

This dialog lets the user select a color:

Figure 4. Selecting a color

Once the user has selected the color, they can use it as the background for the image that is drawn as the background in the scaleBitmap method.

Previewing the Images

The user will want to see the images previewed as each is transferred. This is easily achieved by using a PictureBox component on the application form. We set the background color of the PictureBox to the selected background so that the previewed image looks as much like the transferred one as possible. The image on the PictureBox is set from the scaled image.

Visual C#

previewPictureBox.Image = dest;

Visual Basic

previewPictureBox.Image = dest

The only other thing that we have to do with the PictureBox is to make sure that its SizeMode property is set to Zoom, so that the preview image will fill the preview window exactly.

Showing the Progress

Another useful touch is a progress bar. If a large number of files are being transferred, the user will appreciate being given some indication of how far through the transfer the program has reached. We can calculate the amount of progress by dividing the number of files transferred so far by the total number of files that have been selected. This will give us a fraction we can multiply by 100 to generate a percentage value for the progress bar size.

Visual C#

loadProgressBar.Value = (int)(100 * ((float)fileCount / (float)noOfFiles));

Visual Basic

loadProgressBar.Value = (100 * (fileCount / noOfFiles))

Deadlock Difficulties

The program works well as it is, but it does have one problem. Sometimes the image transfer takes quite a while to complete. During this process, the error tracking provided by Visual Studio might decide that the program has stopped. It then throws an exception that causes the program to fail. Turning this exception off is not difficult; we need to find the "Exceptions" item (on the "Debug" menu item) and then set clear the "Thrown" box next to the ContextSwitchDeadlock exception, so that the dialog appears as below:

Figure 5. Turning off the ContextSwitchDeadlock exception

Once we have done this, the program will run correctly, even with very large transfers.

The Completed Program

The completed program works well. I have successfully transferred 127 pictures onto my Playstation Portable in a single go and I've found that I get lots more pictures on the machine — all 127 fitted into 3.7 MB. However, there are a number of enhancements you might like to consider.

Automatic Playstation Portable Detection

The Playstation Portable has a very distinctive file arrangement on its storage device. In fact you must put your pictures into the \PSP\PHOTO directory or they will not be displayed (use the path in Figure 1 as an example). It would be possible for the program to be made to check through each of the drives on the system, automatically find a PSP device, and configure itself appropriately.

Drag-and-Drop Image Transfer

Rather than pick the images from a file dialog, the program could be made to accept files that are dropped onto the form. It could then scale and transfer those.

Automatic Landscape/Portrait Rotation

The Smartphone has a portrait format display: It is higher than it is wide, which is in contrast to most pictures (which are landscape). This means that the program as written will not make the best use of the screen. It would be useful if pictures could be rotated as they are transferred, so that they fill as much of the display as possible. Alternatively the user could be given the option for all Smartphone pictures to be rotated before transfer, so that they can be shown to best effect.

Follow the Discussion

  • Ian_EIan_E

    I made an earlier post about the threading issue without having fully read the article.  Please accept my apologies, as I see now that you have already addressed the issue.  Thanks for your efforts in putting this out there / educating us.

  • IanIan

    I tried converting about 6 dozen images that started out at 6 megapixels (3072 x 2048; approx 1.8 MB each) and targeting them for my Sony Clie (Palm) PDA (320x320).  After about 1 minute of operation, I received this diagnostic from the debugger:

    ---begin snip---

    ContextSwitchDeadlock was detected

    Message: The CLR has been unable to transition from COM context 0x1a0428 to COM context 0x1a0598 for 60 seconds. The thread that owns the destination context/apartment is most likely either doing a non pumping wait or processing a very long running operation without pumping Windows messages. This situation generally has a negative performance impact and may even lead to the application becoming non responsive or memory usage accumulating continually over time. To avoid this problem, all single threaded apartment (STA) threads should use pumping wait primitives (such as CoWaitForMultipleHandles) and routinely pump messages during long running operations.

    ---end snip---

    I also ran out of Virtual Memory and got an OutOfMemoryException (unhandled).

    It would be really good if you could deal with the threading issue.

  • Rich LRich L

    I've used this but adapted it watch a folder and convert the images into two folders and two separate sizes. Only problem is it is leaving a black line at the top where it has maintained the aspect ratio. Any ideas how to stop this?

  • LuísLuís

    Your program is interesting but compact framework doesn't have folderbrowserdialog class and the openfiledialog is not as flexible as you think. Greetings

  • HajoHajo

    The colors will change if the source image is in CMYK colors. For me #ffffff changed to #fdfdfd

  • HajoHajo

    Some white area (#ffffff) inside the picture changed to #fdfdfd after resizing. How can I get it to keep the exact color?

  • maZZoomaZZoo

    It is a very good idea to set the graphics.InterpolationMode = InterpolationMode.HighQualityBicubic or Bicubic.

    The default resize method in the .NET framework is the same ( fast ) method that IE uses when down scaling images. This will leed to a poor result in many cases.

  • M. AmerM. Amer

    Very interesting utility. I liked the way you explained the code. As a teacher, my hat goes to you.

    M. Amer

  • SoCal SamSoCal Sam

    Just used your article to create some F# material.

    Thanks, it is still working!

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.