Image Manipulation with ASP.NET 2.0

You can display, manipulate, and protect web site images by taking advantage of the .NET 2.0 Bitmap class and Http Handlers.


February 02, 2007
URL:http://drdobbs.com/windows/image-manipulation-with-aspnet-20/197002924

Eric has developed everything from data reduction software for particle bombardment experiments to software for travel agencies. He can be contacted at [email protected].


Ever since learning how to use a 35mm rangefinder camera, I've dreamed of selling my photographs. Now that I've taken some decent pictures with my digital camera, it's time to showcase them on a web site and sell them as stock photographs. I plan to display each image in a variety of resolutions and quality levels, draw copyright notices on the pictures, add EXIF tags, prevent unauthorized users from downloading the full-resolution images, and prevent other web sites from linking to them. Fortunately, ASP.NET 2.0 makes all of this easy.

In this article I show you how to display, manipulate, and protect web site images by taking advantage of the .NET Bitmap class and Http Handlers. Even though image manipulation can be computationally expensive, I display images without compromising on performance. I've included the full source code for a sample web site (available electronically; see "Resource Center," page 5). To build the web site, first extract the source code to a folder. Double-click StockPhotos.sln to launch Visual Studio 2005. Then press F5 or select Debug/Start Debugging to launch the web site. Click the links at the top of each page to see examples of the techniques covered in this article.

.NET Bitmap Manipulation

The web site uses a DLL named "GraphicsDLL" to scale images, draw copyright notices, add metadata to JPEG images, and reduce the quality of JPEG files to conserve bandwidth. GraphicsDLL operates on Bitmap objects because they're easy to manipulate and serve up in ASP.NET applications. The AddWatermark method (Listing One) draws a copyright notice in the middle of a Bitmap as a semitransparent "watermark" (Figure 1). The opacity of the watermark is calculated from the OpacityPercent parameter. The opacity can be any value between 0 (completely transparent) to 255 (completely opaque). After opacity is calculated, Graphics, Font, and Brush objects are instantiated. Then DrawString is called to draw the text.

public static class BitmapUtils
{
  // Draw semi-transparent text in the middle of the Bitmap.
  public static void AddWatermark(Bitmap Bitmap, 
          string WatermarkText, Color TextColor, int OpacityPercent,
          string FontFamily, FontStyle FontStyle, int FontSize)
  {
    int opacity = (int)((255.0f * OpacityPercent) / 100.0f);
    using (Graphics gr = Graphics.FromImage(Bitmap))
    using (Font font = new Font(FontFamily, FontSize, FontStyle, 
                      GraphicsUnit.Pixel))
    using (Brush semiTransparentBrush = new SolidBrush(
                     Color.FromArgb(opacity, TextColor)))
    {
      // Determine the size of the bitmap that will contain the text.
      SizeF size = gr.MeasureString(WatermarkText, font);

      int xMargin = (int)(Bitmap.Width - size.Width) / 2;
      int yMargin = (int)(Bitmap.Height - size.Height) / 2;
      gr.DrawString(WatermarkText, font, semiTransparentBrush, 
                    new Point(xMargin, yMargin));
    }
  }
 ...
  // Add the specified JPEG metadata tag to the Bitmap.
  public static void WriteEXIFTag(Bitmap Bitmap, int TagNumber, 
                                  string TagText)
  {
    Encoding asciiEncoding = new ASCIIEncoding();
    System.Text.Encoder encoder = asciiEncoding.GetEncoder();
    char[] tagTextChars = TagText.ToCharArray();
    int byteCount = encoder.GetByteCount(tagTextChars, 0, 
                                      tagTextChars.Length, true);
    byte[] tagTextBytes = new byte[byteCount];
    encoder.GetBytes(tagTextChars, 0, tagTextChars.Length, 
                     tagTextBytes, 0, true);
    // Cannot just instantiate a PropertyItem because the
    // PropertyItem class does not have a public constructor.
    // Grab the first property item and change its values.
    if (Bitmap.PropertyItems != null && 
        Bitmap.PropertyItems.Length > 0)
    {
      PropertyItem propertyItem = Bitmap.PropertyItems[0];
      propertyItem.Id    = TagNumber;
      propertyItem.Type  = 2;  // ASCII
      propertyItem.Len   = tagTextBytes.Length;
      propertyItem.Value = tagTextBytes;
      Bitmap.SetPropertyItem(propertyItem);
    }
  }
 ...
  // Return an encoder of the specified Mime type
  // (e.g. "image/jpeg").
  private static ImageCodecInfo GetEncoderInfo(String MimeType)
  {
      ImageCodecInfo Result = null;
      ImageCodecInfo[] Encoders = ImageCodecInfo.GetImageEncoders();
      for (int i = 0; Result == null && i < Encoders.Length; i++)
      {
          if (Encoders[i].MimeType == MimeType)
          {
              Result = Encoders[i];
          }
      }
      return Result;
  }
  // Save the Bitmap to the Stream. If it's in JPEG format, save
  // with the specified Quality level.
  private static void Save(Bitmap Bitmap, Stream Stream, 
                           ImageFormat Format, int Quality)
  {
    if (Format != ImageFormat.Jpeg)
    {
      // Save non-JPEG images without changing the Quality level.
      Bitmap.Save(Stream, Format);
    }
    else
    {
      // Adjust quality level of JPEG images.
      // Create an EncoderParameters object
      // containing the Quality level as a parameter.
      EncoderParameters encoderParams = new EncoderParameters(1);
      encoderParams.Param[0] = new EncoderParameter(
              System.Drawing.Imaging.Encoder.Quality, Quality);
      // Save the image using the JPEG encoder
      // with the specified Quality level.
      Bitmap.Save(Stream, GetEncoderInfo("image/jpeg"), 
                  encoderParams);
    }
  }
 ...
}
Listing One

[Click image to view at full size]

Figure 1: Copyright watermark.

The Graphics, Font, and Brush classes implement the IDisposable interface because their objects include resources not managed by the .NET garbage collector. It's important to call an IDisposable object's Dispose method the moment the object is no longer used, so that the unmanaged resources are freed immediately. The objects are instantiated in using statements so their Dispose methods are automatically called the moment they go out of scope. Neglecting to call an IDisposable object's Dispose method reduces your application's performance and scalability.

The JPEG file format lets metadata be embedded as EXIF (EXchangeable Image Format) tags. For example, the EXIF specification (www.exif.org/Exif2-1.PDF) includes defined tags such as Table 1. The WriteEXIFTag method inserts an EXIF tag into a JPEG bitmap. Because EXIF tag text must be in ASCII format, the TagText parameter is converted from Unicode to ASCII by calling encoder.GetBytes. EXIF tags are represented as PropertyItem objects. Because the PropertyItem class lacks a public constructor, you can't directly instantiate a PropertyItem object. Instead, the code takes the first PropertyItem object in the JPEG, changes it to the specified EXIF tag, and inserts it into the Bitmap by calling Bitmap.SetPropertyItem. (To see the EXIF tags in a JPEG file, run the web site. Click the EXIF Tags link. Right-click the image and save it as a file. Then run the DisplayEXIFTags program to display the tags. DisplayEXIFTags is part of the StockPhotos solution.)

Tag ID Field Name
315 Artist (Person who created the image)
3432 Copyright (Copyright holder)
270 ImageDescription (Image title)

Table 1: EXIF tags.

JPEG images are stored with lossy compression that degrades images slightly to reduce storage space. Your web site can save significant bandwidth by reducing JPEG image quality slightly. The Save method stores a JPEG Bitmap in a Stream at a specified quality level. The Quality parameter can range from 100 (best quality, largest size) to 0 (worst quality, smallest size). Saving a JPEG Bitmap to a Stream requires an EncoderParameters object that specifies the quality level. The EncoderParameters object is passed to Bitmap.Save, along with the JPEG ImageCodecInfo object returned by GetEncoderInfo. Click on the web site's Quality link to see the effect of different quality levels. Right-click the images and select Properties to compare their image sizes. The 100-percent quality image has a file size of 71,657 bytes. The 50-percent quality image looks almost identical, but is 60,902 bytes—a savings of about 15 percent. The 30-percent quality image is still acceptable, at least to my eyes, and only takes up 55,006 bytes—a savings of about 23 percent. Below 30-percent quality, the image degradation is excessive.

After a Bitmap has been saved to a Stream, it can be recreated by calling Bitmap.FromStream. If you do this, be sure that the Stream object you used to create the Bitmap object is kept open for the Bitmap's entire lifespan. If the Stream is closed or garbage collected while the Bitmap is still in use, the Bitmap cannot be rendered or saved to a Stream. To see this problem occur, add:

memoryStream.Close();

after the call to Bitmap.FromStream in the second EnhBitmap constructor. Then rerun the web site and watch the exceptions. You'll see ExternalException objects being thrown with messages of "A generic error occurred in GDI+."

The web site uses query string parameters to specify how an image appears. For example, if you type the following URL in your browser:

http://localhost/stockphotos/
  Images/PICT0746.JPG?q=
    95&sx=0.15&sy=0.15&w=&m=False&c
      =True&h=ztwIhRLCwz7m
        ImpJtSkvs8iVBqk%3d


the PICT0746.JPG image is displayed and formatted based on the query string values. For example, the q parameter reduces the image's quality to 95 percent. The sx and sy parameters shrink the image width and height to 15 percent of their original values. The query string parameters in Table 2 can be used.

Query String Parameter Description
q JPEG image quality, 0 to 100.
sx Width scale factor (e.g., specify 0.5 to shrink the width in half).
sy Height scale factor.
w Semitransparent text drawn in the middle of the image.
m A value of true flips the image from left to right, which can be useful for graphics displayed in Arabic and Hebrew HTML pages.
c Specify c=True to cache the image in the server's ASP.NET cache.
ex JPEG metadata tags. Delimited by | characters.
h Hash of query string parameters in the image URL.

Table 2: Query string parameters.

The ImgTagInfo class simplifies creating <IMG> tags with the aforementioned query string parameters. For example, see the copyright page's code-behind (Copyright.aspx.cs):


public partial class Copyright : 
    System.Web.UI.Page
{
  protected ImgTagInfo imgTagInfo = 
    new ImgTagInfo();
  protected void Page_Load
    (object sender, EventArgs e)
  {
    imgTagInfo.FileName  = 
        "Images/2004_03_06_17_51_46.jpg";
    imgTagInfo.Cache     = false;
    imgTagInfo.ScaleX    = 0.50f;
    imgTagInfo.ScaleY    = 0.50f;
    imgTagInfo.Watermark = "(c) E B-T";
  }
}

     

The code-behind instantiates an ImageTagInfo object and assigns values to its properties to specify various image-formatting options. For example, the ScaleX property, which corresponds to the sx query string parameter, specifies that the image width is 50 percent of its original value. The Copyright.aspx page contains a single <img> tag. The src attribute value is filled in by the ImgTagInfo object's SrcAttributeValue property:


<img src="<%= imgTagInfo.SrcAttibuteValue %>" />


When the HTML page is rendered, the <img> tag looks like this:


<img src="Images/2004_03_06_17_51_46.jpg?q=
     100&sx=0.5&sy=0.5&w=(c) E B-T&m=False&c=
       False&h=OcIHylhiGXegf%2b8prE%2fJPITy3sM%3d" />


The purpose of the h query string parameter is to prevent users from changing image URLs to remove copyright notices, increase the image resolution, and so on. For example, if your web site displays thumbnail images for free and charges users for full-resolution images, you don't want users to be able to access full-resolution images by changing the sx and sy query string parameters to 1. The h parameter is an SHA-1 hash of the query string parameters and values, plus a private key. When the image is requested from the web server, the hash is recomputed from the query string values. If it matches the original hash, the image is returned (because it's clear that the query string parameters weren't changed). If you click on the web site's Incorrect Hash link, you'll see what happens when the hash has been manipulated; no image is displayed. If this hashing scheme didn't exist, the web site would be vulnerable to Denial-of-Service (DoS) attacks. Flooding the web server with requests for images scaled to ludicrously large sizes would swamp the web server and drastically reduce the site's responsiveness.

Http Handlers

GraphicsDLL provides all the necessary bitmap manipulations, but how does the web site serve up images in response to HTTP requests? It uses an Http Handler. Http Handlers are classes that implement the IHttpHandler interface to return content to service HTTP requests. If you've programmed ASP.NET applications before, you've used at least one Http Handler—the ASP.NET Page class is an Http Handler that serves up HTML content.

Any class that implements the IHttpHandler interface must have a ProcessRequest method and an IsReusable property. The ProcessRequest method services requests and returns the requested content, along with an HTTP status code. The sample web site's ImageHandler class (Listing Two, Imagehandler.cs, available electronically) is an Http Handler that returns bitmaps manipulated by the GraphicsDLL assembly. The ProcessRequest method first calls context.Response.Clear to clear any HTTP headers and content that might be attached to the response. Then it specifies that it returns content in "image/jpeg" format. ProcessRequest returns a status code of 200 if the bitmap request is successful.

It's rude for a web site to link to images hosted on your web site without your permission, because those links consume your server's bandwidth. But this problem is easy to avoid. The InvalidImageLink method returns True if someone else's web site is linking to your site's images. When InvalidImageLink returns True, ProcessRequest returns a status code of 403 (forbidden). If the link isn't invalid, ProcessRequest calls ProcessBitmap to serve up the requested image.

ProcessBitmap first instantiates an ImgTagInfo object based on the request's URL and query string values. Next, an SHA-1 hash is computed from the ImgTagInfo properties, which correspond to the URL's query string values. If this hash doesn't match the request's hash value, the original query string parameters were changed. In this case, no image is returned, and an HTTP status code of 403 is returned. If ProcessBitmap determines that the query string parameters were not changed, it returns a status code of 200 (success) unless the bitmap file can't be found. The context.Response.Cache.SetExpires call specifies that the image should be cached on the client computer for 60 minutes. Caching images on the user's machine dramatically improves your web site's performance and user experience, since a given image is not repeatedly downloaded as users navigate through your web site.

If ImgTagInfo's Cache property is True, the image may be in the ASP.NET in-memory cache. If the bitmap isn't found in the cache, RenderBitmap is called to create the bitmap with the requested formatting options. The Bitmap.Save call sends the bitmap to the user by writing it to the response output stream. If the Cache property is True, and the image isn't already in the cache, AddBitmapToCache is called to cache the image. Since the image is an EnhBitmap object, which implements IDisposable, AddBitmapToCache uses a CacheItemRemovedCallback delegate to ensure that the object is properly disposed the moment it's removed from the cache. If the bitmap is not cached, its Dispose method is called.

Configuring Http Handlers

Http Handlers must be configured in the web.config <httpHandlers> section. For example, ImageHandler is configured like this:


<httpHandlers>
  <add verb="*" 
     path="*.jpg" 
       type="ImageHandler"/>
</httpHandlers>


Additionally, IIS must have a mapping for the type of the files that the Http Handler serves up. Since ImageHandler processes JPEG files, there must be a mapping for .jpg files. By default, IIS has no .jpg file type mapping. After using the Internet Information Services GUI to configure the sample web site, right-click the web site and select Properties. Go to the Virtual Directory tab and press the Configuration button. Then add a mapping for the .jpg file type (Figure 2). Specify aspnet_isapi.dll for the executable. On my machine, this file is located in the C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 folder. Specify .jpg in the Extension text box. Select All Verbs. Leave the "Script engine" and "Check that file exists" checkboxes checked. If the OK button stays disabled, you're experiencing an outrageous bug in the IIS configuration program. Click on the file path to enable the OK button.

Figure 2: Adding an extension mapping for an Http Handler.

Rendering Bitmaps

On-the-fly image manipulation is easy but has some significant performance costs. If you intend to use this technique on your web site, be sure to test the performance under load to determine if you can achieve your performance goals. Caching images in the ASP.NET cache may improve performance, but only if a small percentage of images and formatting options are requested a large percentage of the time. If your web site doesn't require dynamic image manipulation, you'll achieve much better performance by rendering your images in advance, and deploying the image files to your web site.

The web site project includes the console application PreRenderBitmap. Run this program in a Command prompt. If you run PreRenderBitmap without parameters, it displays a list of the parameters that it expects. The program has parameters for all of the formatting options that the ImageHandler accepts. PreRenderBitmap renders the image with the specified formatting, and then writes it to a .jpg file. If you use PreRenderBitmap to create all the bitmaps that your site displays, there's no need to use ImageHandler. Instead, you can rely on IIS to serve up images, or use the PreRenderedImageHandler. The PreRenderedImageHandler just checks to ensure that an image request is coming from your web site before it serves up the image. You'll need to change web.config to disable ImageHandler and enable PreRenderedImageHandler.

If your web site is hosted on a server that's shared by multiple companies, you may not have access to the IIS configuration to configure mappings for Http Handlers. If you're in this situation, you can use Generic Http Handlers, which require no IIS and no web.config configuration. To add a Generic Handler to your project, right-click the project, select Add New Item, and choose Generic Handler. Unlike Http Handlers that serve up content based on a file type, Generic Handlers are accessed by directly specifying them in URLs. For example, if you create a Generic Handler named "Handler.ashx," refer to it in a URL:


<img src="Handler.ashx?filename= 
   Images/2004_03_06_17_51_46.jpg?q=
      100&sx=0.5&sy=0.5 ..." />


The .NET Bitmap class plus Http Handlers make on-the-fly image generation simple. While caching images in the ASP.NET cache may improve performance, dynamic bitmap rendering has a high performance cost. Unless your web site requires dynamic image generation, you'll achieve optimal performance by rendering all images in advance and copying the image files to your web site. If you use an Http Handler to serve up images, remember to call Response.Cache.SetExpires to ensure that the image is cached on the client machine. After I set up my stock photography web site, you might start seeing my pictures in annoying advertisements littered all over your favorite web sites. I really hope so!

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.