Introduction
Acme Photo Resizer is a simple image processing program designed to resize programs in a bulk operation.
Background
As a way to support the local running community, I take photographs of local races where there is no official photographer. I post them to race web sites and Facebook. When you are posting hundreds of images, you need to reduce the file size to a size that is appropriate for the web-site. Facebook has gotten much better of late, and will display images 2048px wide. What I needed was an easy way to resize hundreds of images at once. While I don't charge for my race photographs, I do like to put a copyright notive and filename on each photograph. That was my motivation for writing this program.
I'm indebted to Code Project contributor Lev Danielyan for his EXIF metadata extraction library
. I use the EXIF orientation tag to rotate images shot in portrait mode.
Photograph resized to 600px wide.
Choosing Images
Acme Photo Resizer has a very basic interface. You specify your resize option, enter an optional Copyright Holder name, choose a set of JPG files and click the process button. The program shows a progress bar and lists the files processed in a list box. It places the resized images into a subfolder named "Resized" in the folder containing the selected JPG files.
The "Orientation" tag in the EXIF tag is used to determine the orientation of the photograph. The rotation is to ensure the photograph is resized with the correct orientation.
Performance and multi-threading
As I was writing this article, it occurred to me that I could improve performamce using multi-threading. Because the program iterates through a set of files, it was fairly straight forward to convert it to use the Parallel
class’s ForEach
method.
Task t = Task.Factory.StartNew(() =>
{
if (Parallel.ForEach(_Files, f =>
{
if (f.EndsWith(".jpg")) {
ResizeImageFile(f, dir, perCent, pixelWidth, pixelHeight, copyrightHolder, flipHorizontal, flipVertical);
ReportProgress(f);
}
}).IsCompleted) {
Complete();
}
});
The tricky part is updating controls in a multi-threaded environment. Controls can only be accessed on the thread that created them. Attempting to access them directly from inside a thread raises an exception. I had a method that updated a progress bar and a list box and I wanted to keep those features. I came across this code project article by Pablo Grisafi and it provided an elegant solution, so I adopted his technique. I appended the following extension method class to the form code file.
static class FormExtensions
{
static public void UIThread(this Form form, MethodInvoker code)
{
if (form.InvokeRequired)
{
form.Invoke(code);
return;
}
code.Invoke();
}
}
That enabled me to implement ReportProgress
quite simply.
private void ReportProgress(string f)
{
this.UIThread(delegate
{
proBar.Increment(1);
lstResized.Items.Add(f);
});
}
I also needed a way to clean up once all the threads had completeted. I used the same technique.
private void Complete()
{
this.UIThread(delegate
{
proBar.Value = proBar.Maximum;
Cursor.Current = Cursors.Default;
this.Enabled = true;
});
}
Resizing an Image
The method that does the resizing uses standard .Net libraries.
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
It is rather large. I built it using various examples I found on the web. I always say "Google is my manual".
private static Image ScaleImage(Image imgPhoto, int? perCent, int? pixelWidth, int? pixelHeight, int orientation, ref Rectangle dest)
{
int sourceWidth = imgPhoto.Width;
int sourceHeight = imgPhoto.Height;
int sourceX = 0;
int sourceY = 0;
int destWidth = 0;
int destHeight = 0;
int destX = 0;
int destY = 0;
if (perCent != null) {
if (perCent == 100) {
return imgPhoto;
}
double nPercent = ((double)perCent / 100);
destWidth = (int)(sourceWidth * nPercent);
destHeight = (int)(sourceHeight * nPercent);
}
else if (pixelWidth != null) {
if (sourceWidth == (int)(pixelWidth)) {
return imgPhoto;
}
destWidth = (int)(pixelWidth);
destHeight = (int)((destWidth * sourceHeight) / sourceWidth);
}
else {
if (sourceHeight == (int)(pixelHeight)) {
return imgPhoto;
}
destHeight = (int)pixelHeight;
destWidth = (int)((destHeight * sourceWidth) / sourceHeight);
}
Bitmap bmPhoto = new Bitmap(destWidth, destHeight, PixelFormat.Format24bppRgb);
bmPhoto.SetResolution(imgPhoto.HorizontalResolution, imgPhoto.VerticalResolution);
Graphics grPhoto = Graphics.FromImage(bmPhoto);
grPhoto.InterpolationMode = InterpolationMode.HighQualityBicubic;
grPhoto.SmoothingMode = SmoothingMode.HighQuality;
grPhoto.CompositingQuality = CompositingQuality.HighQuality;
grPhoto.PixelOffsetMode = PixelOffsetMode.HighQuality;
grPhoto.DrawImage(imgPhoto,
new Rectangle(destX, destY, destWidth, destHeight),
new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight),
GraphicsUnit.Pixel);
if (orientation == 8) {
bmPhoto.RotateFlip(RotateFlipType.Rotate270FlipNone);
}
else if (orientation == 6) {
bmPhoto.RotateFlip(RotateFlipType.Rotate90FlipNone);
}
dest.Width = bmPhoto.Width;
dest.Height = bmPhoto.Height;
grPhoto.Dispose();
return bmPhoto;
}
Flipping Images
Sometimes you end up with images that are flipped horizontally or vertically. In my case, it was old slides and negatives that I photographed from the wrong side. My usual processing program, DxO Optics Pro, does not have a flip funcion, so I added the capability to Acme Photo Resizer. Flipping only takes one line of code. x.RotateFlip(RotateFlipType.RotateNoneFlipX);
Saving Images
The Microsoft Documentation says "GDI+ uses image encoders to convert the images stored in Bitmap objects to various file formats. Image encoders are built into GDI+ for the BMP, JPEG, GIF, TIFF, and PNG formats. An encoder is invoked when you call the Save or SaveAdd method of a Image object." I used the following methods to save files using the JPEG encoder.
private void SaveJpeg(string path, Bitmap img, long quality)
{
EncoderParameter qualityParam =
new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
if (jpegCodec == null)
return;
EncoderParameters encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = qualityParam;
img.Save(path, jpegCodec, encoderParams);
}
private ImageCodecInfo GetEncoderInfo(string mimeType)
{
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
for (int i = 0; i < codecs.Length; i++) {
if (codecs[i].MimeType == mimeType) {
return codecs[i];
}
}
return null;
}
I pass 90L
to the quality parameter. That seems to give good quality and convenient file sizes.
Writing a Copyright notice
Writing to an image involves a few steps. This is the code I use.
Graphics g = Graphics.FromImage(y);
g.SmoothingMode = SmoothingMode.AntiAlias;
int fontSize = (int)(_FontSize * 350.0 / x.HorizontalResolution);
Font font = new Font("Lucida Handwriting", fontSize, FontStyle.Bold);
int picNameWidth = (int)g.MeasureString(picName, font).Width;
g.DrawString(copyrightNotice, font, Brushes.Black, new Point(10, dest.Height - 50));
g.DrawString(copyrightNotice, font, Brushes.White, new Point(12, dest.Height - 48));
g.DrawString(picName, font, Brushes.Black, new Point(dest.Width - 10 - picNameWidth, dest.Height - 50));
g.DrawString(picName, font, Brushes.White, new Point(dest.Width - 8 - picNameWidth, dest.Height - 48));
g.Dispose();
I write the text in black and white to give a slight shadow effect. This means it will still be readable if the background is very light or very dark.
Help
I created a help file in HTML. The Help file is displayed when the Help
button is clicked.
Using the code
This code uses intermediate C# coding techniques. It could serve as a starting point for applications that need to resize images. It also illustrates how to get EXIF data and how to rotate photographs to the correct orientation.
It is multi-threaded and is able to update a progress bar and list box without cross threading issues, thanks to Pablo Grisafi.
As a photographer, I found this program made it much easier to upload large image galleries to sites like Facebook" .
Points of Interest
Acme Photo Resizer illustrates how to resize, rotate, flip and save images. It uses the Parallel
class to improve performance on computers with multicore processors.
History
First Version for Code-Project