Emulating PhotoShop Actions programmatically in ASP.NET with GDI+
Being in the broadcast media business, among the most critical assets we have is the timely distribution of imagery. And one of the "happy little accidents" I so often come across is when people e-mail images of varying size and resolution, both outside of as well as within the hallowed halls of my organization. We constantly get tons of graphics submitted to us from viewers, from other affiliate stations, and from the networks, as well as passing things we shoot out in the field and that we find on the Web along to each other.
Professional organizations send stuff in all sorts of quality levels, sometimes with monstrous file sizes. And Joe Average typically doesn't have the equipment, know-how or technical congeniality to resize/rescale/resave the cool family photo he took on the digital camera his wife gave him. He just wants to see it on the Web. And there's nothing I hate more than having to painfully sit and wait while I download a 9MB e-mail message containing a 1600-x-1200 JPG. Ouch.
The relative chaos of dealing with non-standard images quickly gets messy and needlessly cumbersome. You can set de facto rules on image dimensions and resolutions, but people just won't budge, resulting in large pictures that need to be scaled down, scaled up, shrunk and morphed every which way. It's unreasonable to expect people to conform to a standard, so that's when the magic of technology comes in. This normally requires a graphic artist or person in the know to (1) be present and available, (2) fire up a graphics application like Adobe PhotoShop to do the work and (3) manipulate the images to get them in the right format(s). And the bigger the batch of images, the larger and more time-consuming the job.
Obviously, such a solution is still dependent on a human being, which introduces ambiguity into the equation. So to optimize processes further, I do everything in code with ASP.NET and GDI+ to programmatically emulate such tedious and often laborious tasks.
Most people (myself included) get this type of work done by using PhotoShop Actions. For those of you not in the know, Actions are basically PhotoShop's moniker for macros, empowering a graphic artist with the ability to define a seemingly limitless number of operations, which will later be aggregated/executed with a single-click. So you can define a series of instructions that set up templated formats for images for use on the public Web, within a LAN, on television, in print advertisements, and suitable within e-mail messages, each taking into consideration a particular medium's idiosyncratic display and distribution constraints. In this example, we'll do this in code.
The syntax below preps an image submitted via an ASP.NET WebForm and preps it in a suitable Web format with certain constants:
- Must be a .JPG
- Must not be larger than 80K
- The quality level of the JPG is set to 30
- The resolution of the image is 72 dpi
- The image uses high-quality interpolation
- The image's width can be no larger than 350 pixels
Rather than saving the submitted image directly to disk or posting it in a database, the code first emulates the operations we use in internally in a PhotoShop Action to prepare it to conform to our spec list. Only after the image is prepped, it's saved as a byte array within a SQL Server database table field of type IMAGE.
private void btnSubmitImage_Click(object sender,EventArgs e)
{
// get the stream of data for the image from a server-side Form with an <INPUT> control with an ID of inputUserSubmission
int length = (int)inputUserSubmission.PostedFile.InputStream.Length;
byte[] imageBits = new byte[length];
// read the digital bits of the image into the byte array
inputUserSubmission.PostedFile.InputStream.Read(imageBits,0,length);
// save the byte array as a Bitmap object
MemoryStream ms = new MemoryStream();
ms.Write(imageBits,0,imageBits.Length);
Bitmap unrenderedImage = new Bitmap(ms);
ms.Close();
// manipulate the image according to the specification and save it to the database
WarpImageDimensions(unrenderedImage);
}
private void WarpImageDimensions(Bitmap imageToSave)
{
/* RE-SIZE THE IMAGE ACCORDING TO A FIXED WIDTH OF 350 PIXELS */
Bitmap resizedImage = ResizeSubmittedImage(imageToSave);
/* SET THE RESOLUTION OF THE IMAGE TO 72dpi */
const float res = 72;
resizedImage.SetResolution(res,res);
/* SET THE INTERPOLATION MODE */
Graphics g = Graphics.FromImage(resizedImage);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
/* RE-SET THE IMAGE'S COMPRESSION QUALITY TO "30" */
EncoderParameters encoderParams = new EncoderParameters();
long[] quality = new long[1];
quality[0] = 30;
EncoderParameter ep = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality,quality);
encoderParams.Param[0] = ep;
ImageCodecInfo[] arrayICI = ImageCodecInfo.GetImageEncoders();
ImageCodecInfo jpegICI = null;
for(int x=0;x<arrayICI.Length;x++)
{
if(arrayICI[x].FormatDescription.Equals("JPEG"))
{
jpegICI = arrayICI[x];
break;
}
}
MemoryStream mem = new MemoryStream();
resizedImage.Save(mem,jpegICI,encoderParams);
/* SAVE THE IMAGE TO THE DATABASE AS A BINARY BYTE ARRAY */
byte[] bits = mem.ToArray();
// do database INSERT operations into DB field of type IMAGE here
mem.Close();
g.Dispose();
}
private Bitmap ResizeSubmittedImage(Bitmap bmpIn)
{
// re-size a submitted image while maintaining its aspect ratio
Bitmap bmpOut = null;
int newHeight = 0;
int newWidth = 0;
const int fixedWidth = 350;
const int fixedHeight = 200;
decimal ratio;
// if the image is too small, then just use it as is
if(bmpIn.Width < fixedWidth)
{
ratio = (decimal)fixedHeight/bmpIn.Height;
newWidth = Convert.ToInt32(ratio*bmpIn.Width);
newHeight = fixedHeight;
}
else
{
ratio = (decimal)fixedWidth/bmpIn.Width;
newHeight = Convert.ToInt32(ratio*bmpIn.Height);
newWidth = fixedWidth;
}
bmpOut = new Bitmap(newWidth,newHeight);
Graphics g = Graphics.FromImage(bmpOut);
g.FillRectangle(Brushes.White,0,0,newWidth,newHeight);
g.DrawImage(bmpIn,0,0,newWidth,newHeight);
bmpIn.Dispose();
return bmpOut;
}
The advantage to this type of programming is visible when the distributed nature of the need for imagery kicks in. The base logic can be implemented within an XML web service, so that savvy distant-end developers at business partners can build upload clients in whichever platform they're most comfortable with and get the image to you. For those not-so technically inclined, you can still build a simple ASP.NET image upload client that takes advantage of the HttpPostedFile class for them. In your internal apps, you can easily create a custom HttpHandler to display the image within a databound List control like a DataGrid or Repeater.
Using an HttpHandler is a little more work than just referencing an image stored on a server's filesystem, but the advantage is that you can store previously-viewed images in the .NET Cache API, reducing the total amount of actual database traffic and really improving the user experience when viewing large amounts of images. (Shameless plug: read my article on MSDN about caching data across multiple platforms for more on this subject.)
I've also been forced to come full circle in building tools to help people out. I tried so hard to get people away from the mindset of having to use their Inboxes to send images, but I finally gave in - it's just the way people do things. So I'll bend. I've started extending part of the transmission feature using the Web Service Enhancements 2.0, allowing an XML web service based on similar code to be callable from non-HTTP clients, specifically through e-mail, facilitating SOAP communications through SMTP. This is based loosely on the SOAPMail sample project.
I've also built smart clients that upload images raw and use code not unlike that mentioned here and perform such rendering on the client itself, performing adaptive rendering based on the calling platform/device.
It's really quite cool when you put this type of code into production.
Acknowledgement: even though I assembled a bunch of ideas and concepts into a single service, the crux of the work with the graphics interface wouldn't have been possible without great contributions from Rick Strahl and Chris Garrett, whose wrote outstanding work on the resizing algorithm and programmatically setting the JPG compression levels, respectively. Both have fantastic pieces on using GDI+ in ASP.NET applications. So thanks guys for getting the ball rolling on this one.