Jason Salas' WebLog

On-air and online: making people laugh, making people think, pissing people off

Sponsors

ASP.NET sites that kick ass

Pals with blogs

Podcasts I listen to

July 2004 - Posts

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. 

Dan Wahlin saves my sorry, ignorant butt once again

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:

  1. 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)
  2. 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!

Posted: Jul 25 2004, 10:44 AM by guam-aspdev | with no comments
Filed under:
Mike Hall rules on SportsCenter

Mike Hall, the 22-year-old winner of ESPN's Dream Job contest this past March, started in his first rotation with SportsCenter, the network's flagship program this week.  And he's totally been impressive.  Good suits, good jokes, not trying to be too over-the-top right out of the gate, he's held his own while being a good co-host.

Heck, I've been anchoring news & sports for 5 years, and I picked some things up from the kid.

Go on with your bad self, Mike...kick ass!

Posted: Jul 23 2004, 11:27 PM by guam-aspdev | with no comments
Filed under:
Do you talk to your code?

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?

Good God, I'm a geek...

I was walking through a local mall this evening after doing the 6PM news, and I in so doing, was carrying around no less than 3 mobile devices: my personal LG LX5450 cell phone, my work-assigned 2-way radio/cell phone and my new Audiovox Thera, which I'm test driving for some friends at a local telecomm company.

The fact that I had 3 devices of varying size, weight and color dangling from my belt didn't shock me...the fact that I actually didn't think twice about carrying so much hardware on my person does.  Nowadays, it's not uncommon to see people sporting a pager, cell and/or radio, but I just damn looked odd.  And I became immediately aware of this as I passed a window with reflective tint.  I tried in vain to reconfigure the arrangement of my digital appendages, but I think by that point it was too late.

Hell, I got a call and even I couldn't tell which device it came from!  Maybe it's time to invest in one of those man-purses. 

I gots me some smartphones to play with...

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...

Posted: Jul 22 2004, 07:49 PM by guam-aspdev | with 3 comment(s)
Filed under:
If tuna is brain food, salmon's a lobotomy

I read something years ago that touted tuna as being brain food.  Well, salmon must be the polar opposite, because after a spicy salmon salad from one of my favorite local health bars, Synergy Studios, I couldn't think of a damn thing.  And I was working on a pretty non-complex project.  The old grey matter upon which so much of my work is based just wasn't firing

I gave up eating red meat for lunch a few months back, and it really has helped with keeping my energy levels high (for a challenge, try going out for a steak lunch in the middle of a busy day and then try to do ANYTHING productive for the rest of the day).  You'll drag-ass like never before.

Anyway, a couple trips to the drink machine for some Mountain Dew and King Car Ice Tea and I was back firing neurons...sort of.  We don't get Mr. Pibb out here, and I don't drink coffee, so I get my caffeine where I can.

Great moments in Web history: the dude who "developed" alternating table row colors

I'm completely lost when it comes to determining exactly who came across the notion of using alternating row colors in large lists of tabular data - something that's become a practice in such great use that one would assume that it would wear out its welcome, but most definelty not.  But we would all be wise to tip our collective hat to this person for their discovery.

I recall the early days of ASP 3.0 when the only thing I'd ever use modulus logic for would be to assign BGCOLOR=“#CCCCCC“ to odd-numbered rows.  As alternating row formatting became more popular based on its relative ease of setting up and dynamic results, people started creating components and DreamWeaver plugins just to do it!

My best guess would be to assume that someone in the Web community around 1999 started putting old Excel report formatting tricks to use within web documents.  Heck, the practice became so widespread, a big draw for people just getting into ASP.NET is the fact that the platform makes automates the formatting of such considerations in the List web server controls for those of us who latched onto the “every-other-row-looks-different” mentality.

Jakob Nielsen would (should) be proud.

I keep wanting to develop WinForms apps...but then I don't...

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.

So that's what RAM's used for...

I've been jamming on an app for the last 2 months, a big voter update process that we're only going to use once to keep voting totals in synch across multiple platforms.  The one thing that's bewildered me has been the painfully slow-ass performance. 

In tests on my staging server (a WinXP Pro box w/256MB of RAM), the process cycled through more than 100 candidates and updated a DataSet...and took about 19 seconds to do so.  Which totall had me stumped.  It was a somewhat large job, but nothing extraordinary, and not really that daunting for a processor.  It just ran slow.  I even rebuilt it using a SqlDataReader (I'm using a SqlDataAdapter now), and it still wouldn't fly fast enough/

I finally said “forget it” (but I didn't really say “forget“) and deployed the app to my production server, which sports a couple of gigabytes of RAM and multiple monster processors and the same process ran, without any modifications in about 2 seconds!

D-uh!

More Posts Next page »