TECHNOLOGIES: Image Manipulation | System.Drawing Classes
Display Dynamic Images
Crop, scale, zoom, and pan - all with ASP.NET.
By Bill Wagner
Chances are, none of your Web applications are made of simple text anymore. Instead, your Web visitors want to see pictures of everything they're working with. Sometimes you simply can load these images from a database and deliver them to users. Other times, however, you need to create the images on the fly. Luckily, this is easy to do with ASP.NET because it lets you use all the new graphics features in the .NET Framework and the Graphics Device Interface (GDI+). In this article, I'll show you how to use ASP.NET to generate graphics images on the fly. You'll build a sample based on an archiving system I helped develop for a library, which loads full-page images from old texts. These images are cropped, scaled, and highlighted. Finally, the modified images are delivered to the client.
A Simple Demonstration
This article's demo application shows you how we manipulated the images and delivered them to the client. Figure 1 shows the page I created to demonstrate dynamic image manipulation in ASP.NET.
Figure 1. The buttons on this page let you create and load one of the sample dynamic images.
The page contains three buttons that will show you part of the image; regardless of which you click on, the resulting image is created from the source image. Below the buttons, you can see an image tag that will display the dynamic images. The full system stores the images, the coordinates for articles of interest, and keywords in a database. The sample for this article, however, works only with one image and shows you different regions of the image with some parts of it highlighted. Figure 2 shows you the Web page with the thumbnail image displayed.
Figure 2. This thumbnail image is created from the full-sized source image. Users look at the thumbnail to determine if they have the right image before loading the full article it represents.
Each request follows the same steps:
- Load the source image.
- Apply whatever transformations are necessary to the source image.
- Save the modified image on the server.
- Modify the page to load the dynamic image.
All these features make use of the .NET Framework's System.Drawing classes, especially System.Drawing.Image. The Image class is an abstract class, so you create instances of System.Drawing.Bitmap, a concrete class derived from the Image class. The Bitmap class has a constructor that takes a file and loads the image. Every request starts with loading this sample image:
string Path = Request.PhysicalApplicationPath;
System.Drawing.Bitmap im = new System.Drawing.Bitmap
The Request.PhysicalApplicationPath property returns the physical directory that maps to the application's virtual directory on the Web server. I often use this instead of the Server.MapPath method, but they both accomplish the same basic task. I chose to put my source image in the virtual directory of my Web application. I'll get into this more at the end of the article, along with some shortcuts for a small, downloadable sample.
Create the Thumbnails
The first dynamic image to create is a thumbnail of the full page. The Image class contains a method to do just that: GetThumbnailImage. You pass this method the size of the thumbnail, a delegate, and a pointer to a bitmap surface. In this version of the .NET Framework, neither the delegate nor the IntPtr parameter is used, but both must be present. The IntPtr must be IntPtr.Zero, which is the same as NULL, for the function to work correctly:
private bool thumbCallback ()
// Make a new one, only much smaller:
Size sz = new Size (im.Width / 20, im.Height / 20);
System.Drawing.Image thumb = im.GetThumbnailImage
Once you have the thumbnail, you need to save it somewhere on the Web server so you can deliver it to the client. The Bitmap class contains a Save method that writes a variety of formats. Your Web server might be processing multiple requests from different users concurrently, so you need to save your dynamic images with names guaranteed to be unique across users and unique for the same user across time. I did this by creating the image file name from the session ID and the current ticks in milliseconds. By concatenating the session ID and the number of ticks since 12 a.m. on Jan 1, 0001, you can be sure to have a unique filename:
private string GetImageFileName ()
System.Text.StringBuilder name = new System.Text.StringBuilder ();
return name.ToString ();
To keep it simple, I saved these dynamic images in the root directory of the application:
string name = GetImageFileName ();
string fullPath = Path + "\\" + name;
// Load the image tag:
DynamicImage.ImageUrl = name;
Figure 2 shows the results. In the original application, this let users see small versions of complex graphics images before spending their time generating and downloading larger, higher-resolution versions. By generating the thumbnails dynamically, the original application needed only to store the full high-resolution images in the database. All the smaller images were generated as needed.
Download a Section of an Image
The small thumbnails help users see if they have the correct page, but I doubt anyone can read these. So, it's time to create a more useful image containing a single article from a printed page (see Figure 3). I created large, high-resolution images for the sources, and I shrank them to a reasonable size for ease of use. I also drew borders around the selected article. These scaling and drawing operations give you an inkling of the different features you can add when you need to deliver images to your Web clients.
Figure 3. This image was created by cropping the full image and scaling it to a size that is easy to read.
After loading the source image, create a new bitmap the size of the crop rectangle in the source image, divided by two. This way, it will hold the section of the original image scaled to 50 percent. The source image is 300 dpi, and the code sets all the final images to the same resolution:
System.Drawing.Bitmap crop = new System.Drawing.Bitmap
(cropRect.Width/2, cropRect.Height/2, im.PixelFormat);
crop.SetResolution (300F, 300F);
Now that you have created the destination image, it's time to put the right bits in it. The System.Drawing.Graphics class has a static method that creates a new Graphics context that lets you draw on the image using GDI+ graphics methods. First, draw the selected region of the source image on the destination image, scaling down by 50 percent. The simplest way to do this is to use the Graphics.DrawImage method:
System.Drawing.Graphics g1 = Graphics.FromImage (crop);
Rectangle destRect = new Rectangle (0,0, cropRect.Width/2,
g1.DrawImage (im, destRect, cropRect.X, cropRect.Y,
cropRect.Width,cropRect.Height, GraphicsUnit.Pixel );
After the source image has been copied to the new image, use the Graphics.DrawRectangle method to draw a border around the selected article:
System.Drawing.Pen highlightPen = new Pen (Brushes.Green, 5F);
g1.DrawRectangle (highlightPen, 0,0, crop.Width-3, crop.Height-3);
You can see the final results in Figure 4. The page contains a cropped version of one section of the original image, with the highlight drawn around it. By using similar techniques, you can annotate your own images for your Web users.
The third button shows a mixture of raster and vector graphics capabilities. After creating a destination image that contains the selected article, you need to add the highlight rectangle. The highlight rectangle is an interesting feature. If a pixel in the highlight area is white, you draw it; if it's black, you skip it.
None of my source images had an alpha channel (for transparency), so I had to do the highlighting myself. When you scan an image, not every white pixel ends up exactly white. Any number of imperfections cause pixels to become a light shade of gray. In the same sense, not every black pixel is black; some are dark shades of gray. You need to generate the highlight by examining the brightness of each pixel in the highlight rectangle. If a pixel is close to white, change the pixel to the highlight color. The .NET Framework makes this simple, as well. You can get the color of a pixel by using the GetPixel method. The pixel values are Color objects. The Color class provides a method to get the brightness of the pixel. Simply examine that value and modify the pixel using SetPixel if the value is close to white:
for (int y = highlightRect.Y; y < highlightRect.Bottom; y++)
for (int x = highlightRect.X; x < highlightRect.Right; x++)
Color c = crop.GetPixel (x, y);
if (c.GetBrightness () > 0.5F)
crop.SetPixel (x,y, Color.Pink);
You can see the final results in Figure 4.
Figure 4. When a user has been searching for particular words and phrases, articles return with that text highlighted.
You can see how to mix raster and vector operations to create your final images. The borders are drawn using vector operations on the Graphics object. The highlights are drawn by modifying pixels if they're closer to white than black.
Clean Up the Server
All these actions create a variety of temporary images on the Web server. At some point, you'll need to delete all these images. You can't get rid of them too early, or your users might not receive them; but if you wait too long, you'll have many useless images on your Web server.
I delete all the temporary images when a user session ends. I store the list of image filenames in the session state collection in a System.Collections.ArrayList object. When the page first loads, I see if the session state contains this ArrayList. If it exists, I get the reference to it, but if not, I create it. Anytime I create a new dynamic image, I store the full local path name in the session collection. Later, when the session ends, I delete all these temporary images.
The Global.asax.cs file contains a Session_End event handler already. All you need to do is add the code to that method to retrieve the list of files from the session object and delete each file. The default timeout value for an ASP.NET application is 20 minutes. To test this cleanup operation, you can shorten the timeout: Open the web.config file for your application, find the SessionState element, and modify the timeout value. The value is specified in minutes; I used two minutes for most of my testing.
Here is one important caveat to this strategy: When you run your application in the VS .NET debugger, the SessionEnd method does not get called. The VS .NET process ends the Web server process, then you exit the browser window. So if you are running the sample in the debugger, the cleanup code might not run. To circumvent this problem, leave the browser window open until the session times out.
As I said earlier, I took some shortcuts for this sample, involving where to find the source images and where to put the dynamic images created for the user. You should place the source images in a directory not shared as part of the Web site to prevent users from hacking around and finding them. You should place the dynamic images in a shared directory on the Web server, but in a subdirectory rather than the root application directory. When you need to remove image files by hand because a session did not time out properly, it is much easier if those images are in their own directory. Finally, the full production version uses high-resolution images to produce the crispest possible images at the client, after they are reduced. I did not want you to have to download a 7 mb image to see the sample. So for this sample, the test image is a much lower resolution than the production version of the application.
The files referenced in this article are available for download.
Bill Wagner is SRT Solutions' Windows technology expert (http://www.SRTSolutions.com). He is the author of C# Core Language Little Black Book (The Coriolis Group), an advanced reference for C# developers. Bill has an extensive background in 2-D and 3-D graphics and multimedia software, including developing the video-playback engine used for Disney's "The Lion King Animated Storybook." E-mail Bill at mailto:[email protected].