Using GDI+ in ASP.NET Web Applications, Part 1
GDI+ is a technology that developers generally associate with Windows Forms applications because they use it to draw anything on the screen from custom controls to diagrams.However, you can also use GDI+ in ASP.NET Web applications whenever you want to serve up dynamic images. You can use GDI+ to create dynamic banners, photo albums, graphs, diagrams, and more.
GDI+ is the .NET Framework wrapper assembly for Microsoft's GDI (Graphics Device Interface) technology. In simple terms, you use GDI+ to draw on a drawing surface such as a Window, Control (technically just a window), or Bitmap, among others. Drawing with GDI+ is rather straightforward. You just need a Graphics object that links you to a drawing surface and off you go.
GDI+ uses a Graphics object as the main means to draw on a surface. The Graphics object has methods including DrawLine() or DrawArc() to draw line-based graphics, and FillEllipse() and FillPath() to fill areas with color. If you've used GDI (without the "+") in the past, this is a little different. In conventional GDI, drawing and filling happens in one step, while GDI+ separates the two tasks into individual method calls. In GDI+ you draw with Pens. A Pen is an object that exposes properties such as line color, line thickness, drawing patterns, and end-points such as arrows. You can create your own pens, but for most scenarios the .NET Framework provides all the pens you will ever need. If you want to fill an area, you use a Brush. Brushes are similar to Pens in the sense that they define colors and patterns for the area to fill. However, Brushes come in much more complex configurations. For instance, there are gradient brushes, texture brushes, and brushes based on bitmap images.
You use the Graphics object to draw text strings and you use it to draw and manipulate images. And much, much more. I won't use this article to introduce the intricacies of GDI+. I will simply discuss the functionality in my examples. If you're new to GDI+, I encourage you to read my article, "The Basics of GDI+" in this issue. Assuming that you understand the basics, this article will show you how to apply GDI+ functionality to Web applications.
Let's explore some basic GDI+ features using a Windows application. To follow along with my example, create a new C# Windows Form application in Visual Studio .NET.
When you create a new C# Windows Form application, you start out with a default Form class. As I mentioned above, GDI+ operates through a Graphics object, which the .NET Framework defines in the System.Drawing namespace. This is the namespace that represents GDI+. When you create a Windows Forms application you don't have to worry about using that namespace because all Windows Forms use GDI+ and the namespace is therefore imported by default.
So how do you get access to a Graphics object that allows you to draw on the form? Well, you can create an event handler for the Paint event, which your application will raise every time the operating system asks the form to re-draw itself. The event arguments that you pass to this event handler have a reference to the form's Graphics object. The following sample code uses the Paint event to draw an ellipse on the form:
|private void Form1_Paint(object sender, |
| g.DrawEllipse(Pens.Red, 10,10,150,100);|
In this code I simply call the DrawEllipse() method and pass a red pen as well as the position and the size of the ellipse. The red pen is a default pen defined in the .NET Framework. As you can imagine, the result is not spectacular (Figure 1).
Figure 1: Drawing a simple ellipse on a form.
So far, so good. But what does this have to do with the Web? Up until now, not much. GDI+ can draw not only on forms, but on a number of other "drawing surfaces" such as a bitmap. This interests Web developers because Web browsers (even the oldest of them) can display bitmap images such as GIFs and JPGs. Let's reproduce the above example on a Web page.
Our First Web Example
Start by creating a C# Web application. By default, Visual Studio .NET creates a first Web Form. You cannot use that Web Form to draw on directly because Web Forms are glorified HTML pages that can only do what HTML browsers can interpret. However, you can use the default form to host the drawing example you'll create.
Whatever you create with GDI+ on the Web server will be sent to the client as a bitmap. In fact, the client does not know that the graphic it is about to display is generated on-the-fly using GDI+. Therefore, you can use a simple <IMG> tag to refer to the image. But what is the image's name? Since you'll create the image on-the-fly, the image's name is actually another ASP.NET page. Confused? Just bare with me! Add the following tag to your default Web Form (WebForm1.aspx). To do so, switch to HTML view:
Now, add a new Web Form to your project called "Sample1.aspx." The new form automatically contains some HTML. Since you'll generate an image rather than HTML, you do not want the HTML that was automatically created. Therefore, switch into HTML view and remove the HTML in that file, leaving only the following line in the aspx file (which is required, because it links the aspx file to the code-behind file):
|<%@ Page language="c#" Codebehind="Sample1.aspx.cs" AutoEventWireup="false" Inherits="GDIPlusArticle.Sample1" %>|
In my examples, I use code-behind files for the actual code I'll write. I do this because I think it is cleaner, and also because Visual Studio .NET does so automatically. If you would rather use inline-code (as is the case if you use another editor such as Web Matrix), you can. Where the code resides has no influence on the actual functionality described here.
Right-click on the page and select View Code. The code editor shows some default code. You want the Page_Load event handler method. (Note that, by default, the System.Drawing namespace is included at the very top. In addition, you may want to include the System.Drawing.Imaging namespace to avoid a lot of typing.) The basic idea is that you want to create a blank Bitmap object from scratch, then retrieve a Graphics handle (object) from it, and draw the ellipse on it (see Listing 1).
The Bitmap object is an in-memory image. You generate this image object by creating an instance of the Bitmap class and you then pass the size of the new bitmap as parameters to the constructor.
You retrieve the Graphics object by calling the static FromImage() method on the Graphics class (with the Bitmap object passed as a parameter). Once you have the Graphics object, there is no difference in the drawing code. Drawing on an in-memory bitmap is identical to drawing on a window (or any other GDI+ drawing canvas for that matter). Note that a new bitmap, by default, is all black (all bits are set to 0). Since this most definitely is not the look you're going for, you'll also "clear" the image background by setting the bits to 1 and producing a white background. You didn't have to take this step in the Windows Form version since the default background color was fine (although you can do the same thing if you want to). The rest of the code is identical to the code you used in the Windows Form.
You should next ask, "How do I send the image back to the client?" The answer is: As the current "page." "But," you may say, "ASP.NET pages are HTML pages." This statement is true in most cases because it's the default behavior. In reality you can make an ASP.NET page any type of data. In this case the data type is an image and the image format you'll chose is JPEG. You can change the data type of the aspx page by setting the ContentType property of the Response object to "image/jpeg" (or "image/gif" if you want to send a GIF formatted image). This way the browser that hits the "page" will interpret the returned data as binary image information.
Now you want to return the actual image data by calling the bitmap's Save() method. This method accepts a number of different parameters. One parameter that Save() accepts is a Stream object, and lucky for you, ASP.NET uses a Stream object to send output to the client. Therefore, you simply pass Response.OutputStream as the first parameter to the Save() method. The Save() method's second parameter is the image format you want to return. This is some really nice functionality in GDI+: The Bitmap object can automatically save itself in a number of different formats. In my example, I use JPEG, although there are other options available as well.
That's it! You've processed the page, and you've instructed ASP.NET not to do any further processing by calling the Response.End() method. There is a little bit of cleanup code and then you're all done. You can see the result in Figure 2.
Figure 2: Drawing a simple ellipse for the Web.
Drawing on Existing Images
Once you refresh the page a few times, the excitement fades away and you'll realize that all you did was create a silly ellipse. So let's do something more exciting!
So far I've only shown you how to draw on new images. I've shown you some very useful functionality if you want to create graphics from scratch to draw diagrams and charts. The next examples, however, will show you how to load existing images and manipulate them on the fly. Let's start out with the basics. Let's assume you run a Web site that provides up-to-date news on sporting events. On the home page of your site you want to display a picture of some sports event and the current score (Figure 3).
Figure 3: Rendering dynamic content into an existing image.
You'll use a technique that is very similar to the previous example, except this time you'll load an existing image and you'll use it as the rendering canvas before you stream it to the client (see Listing 2).
To load an image, the Bitmap class has a static method called FromFile()that can load images of all kinds of formats (including JPEGs, GIFs, BMPs, and so on) and return them as Image objects. You really want a Bitmap object. Therefore, you'll cast the returned Image to a Bitmap. Note that in order to load the file correctly on a Web server, you have to use the Server.MapPath() method to get the correct path to the file (which in this example sits in an Image sub-directory).
Once you have the image in memory, you can use it as a drawing canvas the same way you did before. This time, however, instead of rendering a vector drawing, I'll show you how to render text using the DrawString() method of the Graphics object. The first operation, "The Fans Go Crazy!" is relatively straightforward. The code will simply draw a string at position 5,5 using a previously instantiated Font object as well as a light-cyan color, which is one of the available standard colors.
It is trickier to render the score because you want to render it into the bottom-right corner of the image. You'll define a bounding rectangle for the string. Think of the rectangle as similar to a textbox object in Word. The bounding rectangle is defined (you guessed it) through a Rectangle object. You'll make the size of the rectangle identical to the size of the bitmap you want to draw on. You'll use the DrawString() method to render the string into the defined rectangle by passing the Rectangle object as a parameter. Finally, you'll use a StringFormat object to instruct the DrawString() method to render bottom and right-aligned. You can set horizontal and vertical alignment through the StringFormat object's Alignment and LineAlignment properties, respectively. Note that the available options are a bit confusing. In this example, StringAlignment.Far is the correct setting for both properties. This is really only a fancy way to specify left and bottom alignment in a left-to-right and top-to-bottom text system (which is the system that readers of English use).
If you look very closely at Figure 3, you will see a little copyright notice rendered vertically at the left edge of the image. I did this by transforming the coordinate system of the drawing canvas 270 degrees using the RotateTransform() method of the Graphics object. From that point on, everything that would normally be drawn horizontally, left-to-right, will be drawn vertically, bottom-to-top. For the most part, this makes rendering text (or anything else for that matter) at an angle really straightforward. It gets tricky trying to figure out where to position things?everything that used to be right is now up. Everything that used to be down is now left. This means that the physical bottom of the image is at a negative vertical position in the virtual coordinate system I created.
Confused? Don't worry about it too much for now (or check out my "Basics of GDI+" article in this issue). Just assume that the bottom left corner of the image now is at a position of Height*-1,0. In other words, the X coordinate is the negative height of the image. Is that clearer? That's how it works. To keep confusion to a minimum, call ResetTransform() after the string drawing operation, which will reset the coordinate system to normal and avoid further brain-twisting.
Rendering Graphical Buttons
One maintenance nightmare that occurs on many Web sites is rendering graphical buttons. Figure 4 and Figure 5 show graphical buttons I use on my personal site (www.MarkusEgger.com). You can probably already imagine how this is done. Listing 3 shows the detailed code (this time in Visual Basic .NET).
Figure 4: A modern looking set of graphical buttons
Figure 5: A more "classical" look.
Rather than load the background image for the button and then draw on it, I create a blank bitmap with a default size from scratch. This guarantees that the size of the image stays the size I want it to be. Because I use these buttons in the navigation area, I don't want the buttons to be different sizes. However, this is specific to the current example, and you may choose to adjust the size based on the background image. You can use the same technique I described in the sports Web page example.
Once I create the new bitmap in memory, I load the background image as well. This time, however, I immediately convert the image into a Brush. This allows me to fill any shape with a repetitive pattern of the background image. If the background image is at least as large as the canvas, the image will be shown only once, but if I later choose to change the button style and create some type of texture for the background, the current implementation would automatically handle that.
The rest you already know at this point. I simply render the desired string using a certain font. I define the string using a URL parameter. This way I can insert a text button into any HTML page like this:
This also works during design time. If you add this image to an aspx page and look at it in the editor, you will see the image render with the correct text. As soon as you change the URL parameter, the text will change. Visual Studio .NET runs your code during design time. Very cool!
Also please note, in this example, that I set a property called TextRenderingHint on the Graphics object that instructs the Graphics object to use a text smoothing algorithm to draw strings. By default, GDI+ will use the system settings. If the system setting calls for Clear Type rendering, for instance, the quality won't be great on clients that do not use LCD monitors. Usually, AntiAlias is the best choice for dynamically generated Web images.
I want to briefly discuss image quality. Consider the rounded red rectangle in Figure 6. The rectangle highlights an area but it is not part of the actual image that gets generated. It is generated dynamically similar to the other images that I've shown you how to create in this article. The main difference is that the rectangle is on a white background, which makes it harder to make the text look crisp. Also, the chosen color ends up looking "rastered" using the default rendering settings. Therefore, I took some extra steps to make sure the quality ends up at an acceptable level.
Figure 6: The highlighted heading is a dynamically generated, high quality image.
Listing 4 shows the code that generates this image. At this point you already are familiar with most of the code that generates the image. The main difference is that I don't just call the bitmap's Save() method. I first retrieve and configure a Decoder object. GDI+ uses decoders to load and save image information. You already have a number of default decoders installed on your system, including decoders for JPEG, GIF, BMP, WMF, and more. In Listing 4, I first retrieve a collection of all installed image decoders using the ImageCodecInfo.GetImageDecoders() method. Then, I iterate through the entire collection until I find the one that matches the content type that I want to stream to the client (JPEG in this case). GDI+ would automatically use this decoder to save the file to the output stream. I want to retrieve it manually so I can set the parameter for the image quality (.NET can render JPEGs at different quality levels). I instantiate a new EncoderParameters collection with a length of 1 item. I then create a new EncoderParameter item and assign it to the first item in the collection. When I instantiate the parameter I can specify the parameter type (Encoder.Quality) as well as the setting (500). This lowers the compression of the generated JPEG image and therefore increases the quality substantially. You may notice that the syntax to create this collection is a bit "odd." I am not sure why this collection doesn't work similarly to regular collection syntax. But, once you get past the weirdness, it works quite well.
Finally, you save the image to the output stream with the retrieved encoder object and the generated encoder parameters.
An alert CoDe Magazine reader may notice that I did not use a default Windows font. I use a font I purchased from a third party and installed on my company's Web server. Our system administrator was a little confused by that at first and pointed out that this wouldn't work because the font wouldn't be streamed to the client machine. He is correct, but it is still OK to use non-standard fonts because the font is only used on the server to generate the image. Only the resulting image gets streamed to the server, and therefore, you only need the font on the server.
Loading Images from a Database
Web server directories are often a mess because the images required by a Web application generally have to be available in some type of Web-accessible folder. Imagine maintaining an item database such as Amazon.com's list of books with all the different images sitting in folders on the hard drive. Luckily a database such as SQL Server can store bitmaps in binary fields. (In SQL Server, the field type is actually called "Image".) Let's assume you have an image in an image field of an ADO.NET DataSet. How can you stream that information to the client? You could simply take the binary information and write it into the output stream. While this works fine, it doesn't give you the opportunity to manipulate the image before you send it to the client. To do that, you need to load the binary image information into a memory stream, and then you can turn the binary image into a bitmap object:
|Dim stmFile As New MemoryStream()|
|Dim objWriter As New _ BinaryWriter(stmFile)|
|objWriter.Write( _ dsImg.Tables(0).Rows(0)(0))bmpFile = New Bitmap(stmFile)|
Note: This example assumes that the data resides in a DataSet called dsImg. You'll find the image field in the first field of the first row of the first table in the DataSet. You'll need to adjust this to match your data structure.
Once again, GDI+ will pick the appropriate image decoder to load the image as long as it is in a recognized format. Once you've loaded the image into memory, it is in a format-neutral form and you can stream the image to the client as any type of image format that your browser recognizes (typically JPEG or GIF). This lets you store images in the database as TIFF, BMP, or another format that browsers don't recognize. You can simply stream them in GIF or JPEG format to make sure all browsers can display the image.
You can also manipulate the image itself. Naturally, you can add to the image. Perhaps you want to render a copyright line. You can also manipulate the image itself. You could make color adjustments for instance. Or what if the image is too large to display? Let's say you want to make sure no image is wider than 500 pixels. Here's how that's done:
|Dim intMaxWidth As Integer = 500|
|If bmpFile.Width > intMaxWidth Then|
| Dim dblRatio As Double = _ bmpFile.Width / intMaxWidth|
| bmpFile = New Bitmap(bmpFile, _|
| bmpDisplay.Width / dblRatio, _ bmpDisplay.Height / dblRatio)|
The code first compares the Width property of the image to the maximum allowed width (in this case 500). If the image is wider, the code calculates a shrink-ratio and then generates a brand new image based on the current image, but with a new height and width. GDI+ will resize the image for you.
By the way, if you are reading this article online, you see this technology at work. The Web site for CoDe Magazine (Figure 7), keeps all images (such as cover art, and images that go with articles) in a database and streams them back using this technique. Just right-click on one of the images in this article, and check out the URL. You can even see the original image name, which is probably not a JPEG.
Figure 7: The cover image on the CoDe Magazine homepage is loaded from a database and resized on the fly.
A Simple Photo Album
I'm going to take what you've learned so far and write a simple photo album application. Let's say you want to write an ASP.NET page that will display all the image files in a directory, and add formatting to display the images as you would see them in a strip of film. You can easily retrieve a list of all files using the following command:
Once you retrieve a list of files, you can generate a list of <IMG> tags, such as the following:
Listing 5 shows the code to create the film strip effect. It will first create a black drawing canvas. Then the code creates a second image to use as a brush to draw the whole at the top and the bottom of the film strip. This bitmap is as tall as the main film strip, but only as wide as a single hole. The code draws the holes using rounded white rectangles with a thick enough Pen to make them look like filled rectangles (or holes). Finally, the code creates a new texture brush based on that image and fills the entire area with that texture. Voila! You've created a film strip.
Next the code loads the actual image and determines its size and orientation. Based on that information, the code calculates the shrink-factor for the thumbnail image, which the code creates then draws on the canvas with an appropriate placement to center the thumbnail on the film strip. Finally, the code renders a copyright notice and streams the file to the client.
Note: Bitmap objects have a native method to generate thumbnail images. Depending on your situation, you may be better of using the Bitmap object's native method to create thumbnails. This technique often performs better because the method can retrieve thumbnail information that may already be in the image. In this example however, I chose not to do that in order to have more control over the generated thumbnail.
You can see the result in Figure 8, or on my personal Web site (www.MarkusEgger.com). If you go to my Web site and look at the pictures from Chichen Itza (as shown in Figure 8), you will see about 50 thumbnail images. Each of those images (before my code turns them into thumbnails) is about 2MB in size. When you hit that page, you will make ASP.NET plow through about 100MB of data. The resulting performance is very reasonable. In fact, on my local machine, the Web site generates the thumbnails with the extra film strip effect faster than Windows Explorer can display a thumbnail view of the same folder. It's quite amazing.
Figure 8: Rendering fancy thumbnail images with a film strip effect.
What About Performance, Anyway?
Despite the great performance shown by GDI+, it is clearly slower to load images from disk than it is to generate and manipulate them on-the-fly. For many Web sites, the difference is probably not a problem, but if you need a really fast and scalable site, you may need to consider alternative solutions.
For example, it would be better to store the generated image of the photo album to disk the first time around, and then use that file whenever it already exists. I like this solution because once you've generated a thumbnail image, you probably won't change it.
For more dynamic images, such as the sports Web site, you might choose to let ASP.NET cache the information for a short amount of time. ASP.NET has a very powerful caching mechanism that allows you to enable caching at the page or control level for defined amounts of time. This way you can cache the page that generates the dynamic image for a few minutes at a time, which can make a big difference on a busy site. You can also use caching on a URL parameter basis, which lets you cache images that use query string parameters individually. This would work for the image button examples. (In this case, you probably want to cache the output for more than just a few minutes).
The subject of ASP.NET caching is way beyond the scope of this article. It also isn't specific to image generation. Please refer to the caching documentation or other articles and books for more information on this subject.
I've only managed to scratch the surface of what's possible with GDI+ on Web pages. So far I've focused mainly on image manipulation. Part 2 will demonstrate how to generate drawings, such as graphs and diagrams.
If you have any questions about this article, feel free to email me.