Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Image Manipulation with ASP.NET 2.0


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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.