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.
I've lost count of how many times Dan Wahlin, either directly through e-mail or indirectly through his writing, has helped me out with XML-based problems in my projects. I was able to tap Dan's wisdom yet again this morning as I sussed out a nagging problem with inferred schemas and XML's nature to assume all fields are strings in lieu of a more formal validation structure.
I was able to figure out a curious problem I had with overriding the default inferred XML schema applied to a DataSet, which was ruining what was intended to be numeric sorting. I took a look at Dan's timeless, seminal work "XML for ASP.NET Developers" and literally slapped myself up the head when I discovered found 2 key points:
- the XML Schema needs to be loaded first into a DataSet before the XML itself is read into the DataSet (not the other way around)
- you need to call DataSet.AcceptChanges() - D-UH!
I was previously trying unsuccessfully to apply conditional XS:INT and XS:DOUBLE typing to certain fields using the following construct:
DataSet ds = new DataSet();
ds.ReadXml(Server.MapPath("results.xml"));
ds.ReadXmlSchema(Server.MapPath("results.xsd"));
ds.WriteXmlSchema(Server.MapPath("results-2.xsd");
...and here's the code that got it working:
DataSet ds = new DataSet();
ds.ReadXmlSchema(Server.MapPath("results.xsd"));
ds.ReadXml(Server.MapPath("results.xml"));
ds.AcceptChanges();
Then, running a test with WriteXmlSchema() shows the data typing has taken effect, and the page in which the code executes clearly shows that the data is properly sorted numerically.
Thanks Dan! I owe you some serious BBQ if you ever head out this way!
Being a musician, I've learned that only after reaching the point of maturity in your own skill set in general and in particular with an instrument, do you develop a relationship that exponentially amplifies your ability to create. Most expert players can all attest to the quasi-sexual experience one can have while playing a certain song with a certain instrument - it's been said that the great B.B. King openly cries each and every time he performs "The Thrill is Gone" on his prized Lucille. There's a real emotional bond created between man and machine.
As a software developer, I've bonded with the syntax to which I affix myself for countless hours. Some programmers talk to their PCs....I talk to my code. I see the machines on which I work as vessels for the real tool...the code itself, and I get right into conversations with my beloved scripts. I've found it helps the productive process to engage in a running narrative that serves to aide as a self-imposed quality control mechanism, if you will. And I don't assume I'm alone.
I find myself having a tendency to be quite vulgar when working on silly, mundane helper methods (like string manipulation routines), while I'm more loving and affectionate with ADO.NET and database communication code, and downright abusive and demeaning when it comes to XSLT. In contrast, I'm totally submissive and "whipped" when it comes to anything JavaScript (largely because I suck at it).
How about you?
One of Guam's premiere telecomm companies, GuamCell Communications, was nice enough to let me borrow two brand-freakin'-new smartphones, the first to be released for public sale on Guam. I got my hands on the Audiovox Thera PocketPC with the latest version of Windows CE and the Treo 600 with the latest PalmOS.
I'm supposed to test/evaluate/criticize/compare/contrast the units and their relationship with Guam's new wireless data services for the TV segment on technology I host every Tuesday, “Tech Talk with Jason Salas.“ It's basically me being goofy and playing with different toys, and trying to explain complex concepts in plain English.
I admittedly haven't played with the PalmOS since the grayscale days of my old Palm III, and since joining the Church of Gates, I've naturally been playing with and developing for Windows operating systems.
I'm lucky to have friends in the know...my good friend Jamil Justice is my company's creative director, and as an artist, is (surprise, surprise) a Mac guy. And as such, he's more inclined to the Palm stuff, so we're going to do a tag team report next week.
In the meantime, it's hella fun playing with this stuff. I'm already building some administrative services to run over the phones, since they can both tap the actual World Wide Web, not a WAP offshoot. And I've been working with GuamCell on developing a business plan for hosted LAN services, wherein they provide a private gateway to a library of apps we write and host for our network ff of our web server.
If I can ever stop playing MP3s on the Audiovox unit, maybe I'll get around to working on the story...
I'm a web developer, hard to the core. Every few months, I get the “expand thine own skill set” bug and start to dabble in Windows Forms programming, but then almost as quickly dismiss it because it's so damn hard and frustrating.
Label me impatient and/or ignorant, but sometimes, I don't want to build every single menu, MDI element or be responsible for doing the simple-yet-complex computations required to get the precise positioning of controls on a Windows application. It's then when I realize what a blessing we have with the browser being pre-built, and then re-commit myself ot my chosen craft.
At least for another few months.
I'd venture to say that there are a significant amount of desktop/console devs that do web stuff that the converse, and this shouldn't come as a surprise to anyone. And if you're a server-side developer on any platform (ASP, ASP.NET J2EE, PHP), there are more than enough topics to keep you from becoming bored with the redundancy of HTML and writing programs that generate it. And heck, innovations in web technology are bridging the divide between what's truly “desktop only” capability and that which can be deployed, managed and accessed via the Web.
Count your blessings, and praise the almighty URL.