About six months ago, I started investigating how to perform lossless JPEG bitmap image rotation under WPF and started a thread on The Code Project to see if anyone had a solution.
I also found (and contributed to) this thread on the Microsoft .NET Framework Developer Center.
The last post in the latter thread was my own, dated June 9, 2009, with no interest expressed since. Perhaps everybody was waiting until .NET 4.0 came out to see if the problem would be addressed. Unfortunately I see no evidence that it has. Perhaps it wasn't addressed because it is relatively easy to add GDI+ code to your WPF app to solve the problem. Unfortunately, the GDI+ solution is only good for JPEG, because GDI+ doesn't support Windows Media Photo Files (.wdp), which has compression superior to JPEG and is supported in WPF.
In any case, I was getting increasing pressure to add lossless JPEG rotation to my application from beta testers and now that it looked as if .NET 4.0 wasn't going to be of any help, I decided it was time to bite the bullet and solve the problem in my WPF app through GDI+ code. A side-benefit was that the entire image metadata block was transparently transferred intact to the destination image, except for the Orientation metadata item which I had to set to 1 to indicate that rotation was no longer necessary in the JPEG image file.
A caveat is that lossless JPEG image rotation is only mathematically possible if pixel dimensions of both width and height are multiples of 16, because the basic units of JPEG bitmaps are pixel arrays 16x16. That's not that big a deal because the native formats of commercial digital cameras meet this restriction.
The first thing you have to do to integrate GDI+ into your WPF application is to include the System.Drawing
and System.Drawing.Imaging
namespaces into your assembly. To enable that, you have to add a reference to the System.Drawing
.NET assembly. After that, you can compile references to GDI+ classes such as System.Drawing.Image
and several related classes that you'll need.
The only code I'm going to quote is the specific functions that access GDI+: RotateImage
and helper function, GetEncoderInfo
. But higher level code that calls RotateImage
iterates through a list of a large number of image file paths in a folder hierarchy of indefinite depth. Code to do that enumeration and iteration is just standard C#/WPF code that need not distract us here. But because there could be thousands of JPEG image files in the enumeration, you have to execute the code in a separate thread to keep the UI responsive. I do that through the WPF BackgroundWorker
thread, the details of which are also beyond the scope of this tip.
Worth mentioning however is the EncoderValue
parameter input to RotateImage
. This is one of three values that is determined from calling code that looks at the Orientation
metadata value in each JPEG image file. The GDI+ EncoderValue
s of interest are TransformRotate90
, TransformRotate180
, and TransformRotate270
, depending on whether the Orientation
metadata value is 6
, 3
, or 8
. Also not shown is how I get the Orientation
metadata value from each image. That's just WPF twiddling and diddling with the BitmapMetadata
and JpegBitmapDecoder
classes that are also beyond the scope of this tip.
Here then is the code for RotateImage
and the helper function, GetEncoderInfo
:
private void RotateImage(string filename, EncoderValue encoderValue)
{
System.Drawing.Image image;
PropertyItem[] PropertyItems;
string filenameTemp;
var Enc = System.Drawing.Imaging.Encoder.Transformation;
EncoderParameters EncParms = new EncoderParameters(1);
EncoderParameter EncParm;
ImageCodecInfo CodecInfo = GetEncoderInfo("image/jpeg");
image = System.Drawing.Image.FromFile(filename);
PropertyItems = image.PropertyItems;
PropertyItems[0].Id = 0x0112; PropertyItems[0].Type = 3;
PropertyItems[0].Len = 2;
byte[] orientation = new Byte[2];
orientation[0] = 1; orientation[1] = 0;
PropertyItems[0].Value = orientation;
image.SetPropertyItem(PropertyItems[0]);
filenameTemp = filename + ".temp";
EncParm = new EncoderParameter(Enc, (long)encoderValue);
EncParms.Param[0] = EncParm;
image.Save(filenameTemp, CodecInfo, EncParms);
image.Dispose();
image = null;
GC.Collect();
System.IO.File.Delete(filename);
File.Move(filenameTemp, filename);
System.IO.File.Delete(filenameTemp);
}
private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
int j;
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for (j = 0; j < encoders.Length; ++j)
{
if (encoders[j].MimeType == mimeType)
return encoders[j];
}
return null;
}