Rendering Images in ASP.NET Directly from Your Database

Introduction

Time and again I have read articles about loading images on ASP.NET pages. All of the articles I have read suggest a variation on storing file system paths in the database and loading the URN (or path) to the image. But why should images be treated differently than text? Images are just data and one wouldn't store paths to text or numeric data.

In this article, you will learn how to store images in your database, directly load them to your ASP.NET pages, and some of the benefits that can be derived from this approach. If you are sure you know how to do this, read next week's article, but consider skipping to the end of this article. You are sure to learn a couple of techniques you may not be familiar with and there is a great book recommendation.

Disclaimer: Windows Presentation Foundation (WPF, aka Silverlight) may support loading images directly from the database, when you adopt Silverlight.

Understanding What Many Programmers Do Now

Everybody uses images in ASP.NET. The de facto standard for bitmaps, JPGs, and GIFs (collectively images) in ASP.NET is to store the URL/URN to the image in the database and bind the path to the image's source property. The technique described herein is relatively easy, but almost every other kind of data is stored in the database itself and images can be too.

Loading Images Directly from Your Database

First, you know that images can be stored in the database as an Image type (at least in SQL Server, and similar types exist for Oracle or whatever). You also know that you can store Image fields and retrieve them. The trick is how to get them in the <img> HTML control or the Image Web control. Because web controls are just little HTML generators, you can use either the HTML control or the ASP.NET Image control with database image binding.

For this example, I will be using .NET 2.0, Visual Studio 2005, and SQL Server 2000. I assure you this technique works for the next version of these products too. I will also be using the AdventureWorks2000 sample database because it has images in the ProductPhoto table and should be available to many of you.

Note: The AdventureWorks2000 ProductPhoto table uses .gif images. If you try to load a .gif image directly, you will receive a System.Exception with the following error: "A Graphics object cannot be created from an image that has an indexed pixel format." This article demonstrates how to resolve this error in a couple of ways. Generally, I use JPGs because they seem to be a little more manageable here.

Storing and Retrieving Images

An insert statement will load an image to a database field. Listing 1 shows code that will load an image from a database to a custom object (in Listing 2) containing an Image field, not the byte array you get back from the database.

Listing 1: The Data Access layer that constructs the ProductPhoto generic list.

Imports Microsoft.VisualBasic
Imports System.Data
Imports System.Data.SqlClient
Imports System.Collections.Generic

Public Class ReadProductPhoto

   Private Shared connectionString As String = _
      "Data Source=BUTLER;Initial Catalog= _
       AdventureWorks2000;Integrated Security=True"
   Private Shared sql As String = _
      "SELECT ProductPhotoID, LargePhoto FROM ProductPhoto"

   Public Shared Function  GetProductPhotos() _
      As List(Of ProductPhoto)

      Using connection As SqlConnection = _
         New SqlConnection(connectionString)
         connection.Open()
         Dim command As SqlCommand = New SqlCommand(Sql, connection)
         Dim reader As SqlDataReader = command.ExecuteReader

         Dim list As List(Of ProductPhoto) = _
            New List(Of ProductPhoto)
         While (reader.Read())

            Dim id As Integer
            Dim image As Byte() = Nothing

            If (reader("ProductPhotoID") Is System.DBNull.Value) _
               Then Continue While
            id = reader.GetInt32(0)

            If (reader("LargePhoto") Is System.DBNull.Value = False) _
               Then image = CType(reader.GetValue(1), Byte())
            End If

            list.Add(New ProductPhoto(id, image))


         End While

         Return list
      End Using
   End Function

End Class

Listing 2: The abridged ProductPhoto class (that contains enough information for demonstration purposes).

Imports Microsoft.VisualBasic
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO


Public Class ProductPhoto

   Public Sub New(ByVal ProductPhotoID As Integer, _
                  ByVal LargePhoto As Byte())
      Me.FProductPhotoID = ProductPhotoID
      SetLargePhoto(LargePhoto)
   End Sub

   Private Sub SetLargePhoto(ByVal photo As Byte())
      Dim stream As MemoryStream = New MemoryStream(photo)
      LargePhoto = Image.FromStream(stream)
   End Sub

   Private FProductPhotoID As Integer
   Public ReadOnly Property ProductPhotoID() As Integer
      Get
         Return FProductPhotoID
      End Get
   End Property

   Private FLargePhoto As Image
   Public Property LargePhoto() As Image
      Get
         Return FLargePhoto
      End Get
      Set(ByVal value As Image)
         FLargePhoto = value
      End Set
   End Property

End Class

The key to getting the SQL Server Image type into a System.Drawing.Image type is to load a stream and construct the image from the stream, as shown in SetLargePhoto. The problem is not getting a viewable image; the problem is actually display the viewable image. If you initialize a GridView of List(Of ProductPhoto) to a GridView right now, it looks empty—no image, as illustrated in Figure 1.

Figure 1: There is no mechanism for assigning an Image field to an Image control (as represented by the Image field in the GridView shown). HTML and web controls want URLs.

Rendering an Image to a UserControl

Now that you have the Image in an object, you have to figure out how to render it. The answer is to insert the image into the HttpResponse stream. You'll add a UserControl to your project and have the UserControl render the image for you (see Listing 3).

Listing 3: You render the graphic to the HttpResponse stream in a UserControl.

Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO

Partial Class ImageControl
   Inherits System.Web.UI.UserControl

Protected Sub Page_Load(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Load

   End Sub

   Private FImage As Image
   Public Property TheImage() As Image
      Get
         Return FImage
      End Get
      Set(ByVal value As Image)
         FImage = value
      End Set
   End Property

   Private Sub RenderImage()
#Const BestForGif = True
   If (FImage Is Nothing) Then Return
#If BestForGif Then
      ' this approach seems to work best for gifs - so we are
      ' switching back to a byte array - jpegs can use the code below
      Dim stream As MemoryStream = New MemoryStream()
      FImage.Save(stream, ImageFormat.Gif)
      Response.BinaryWrite(stream.ToArray())
#ElseIf ResolvesGifPoorQuality Then

      ' resolves: "A Graphics object cannot be created from
      ' an image that has an indexed pixel format too" for gifs
      ' but yields a poor result. Use for jpegs or bmps

      Dim b As Bitmap = New Bitmap(FImage.Width, FImage.Height)
      Dim g As Graphics = Graphics.FromImage(b)
      g.DrawImage(FImage, 0, 0)
      Response.ContentType = "image/gif"
      b.Save(Response.OutputStream, ImageFormat.Gif)
      Response.End()

#Else    ' simple for something like gifs

      Response.ContentType = "image/jpeg"
      FImage.Save(Response.OutputStream, IMageFormat.Jpeg)
      Response.End
#End If

   End Sub

   Protected Sub Page_PreRender(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.PreRender
      RenderImage()
   End Sub
End Class

After adding a UserControl to your project, add an Image property and a method that sends the image to the HttpResponse stream. RenderImage shows a couple variations of the image rendering. GIFs use indexes into a color palette, so the best result seems to be simply adding the raw bytes to the response stream. If you use something like JPG files, you can use the last #Else condition block, which is pretty straightforward.

Rendering Images in ASP.NET Directly from Your Database

Placing the UserControl on a Designated Page

You have the UserControl. Now, youneed a page. The reason for the UserControl and Page is that writing to the HttpResponse stream directly is destructive; if you wrote right to the page that ultimately will contain your images, you would wipe everything else out of the page. (I suspect there is a way to inject the image in a non-destructive way, but I haven't quite perfected it yet.)

The next step is to add a page, drop the UserControl on the page, and coordinate associating each unique ID with the UserControl on each page. Listing 4 contains the code for the page that will play the role of the image source.

Listing 4: The Page_Load event coordinates which image is shown in any specific page and UserControl pair.

Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Collections.Generic

Partial Class ProductImage
      Inherits System.Web.UI.Page

Protected Sub Page_Load(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles Me.Load

      If (Request.QueryString("id") Is Nothing) Then Return
      FId = CType(Request.QueryString("id"), Integer)
      Dim list As List(Of ProductPhoto) = CType(Session("data"), _
         List(Of ProductPhoto))
      Dim o As ProductPhoto = list.Find(AddressOf Match)
      If (o Is Nothing = False) Then
         FImage = o.LargePhoto
      End If

   End Sub

   Private FId As Integer

   Private Function Match(ByVal o As ProductPhoto) As Boolean
      Return o.ProductPhotoID = FId
   End Function

   Private FImage As Image
   Public Property TheImage() As System.Drawing.Image
      Get
         Return FImage
      End Get
      Set(ByVal value As System.Drawing.Image)
        FImage = value
      End Set
   End Property


   Protected Sub Page_PreRender(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.PreRender
      ImageControl1.TheImage = FImage
   End Sub

End Class

The Page_Load event is looking for a QueryString indexed by the string "id". The QueryString—~/ProductImage.aspx?id=1, for example—indicates which key is used to determine which record this page instance is responsible for. Next, you get the List(Of ProductPhoto) objects from session, and finally use List(Of T).Find to search the list for the page's ProductPhoto. Find takes a predicate—think a where condition—and sets the found Image property of the contained Image control.

Figure 2: A visualization depicting the relationship between the page containing the rendered image and the <img> control that uses the page as the image source.

Loading the Page as an Image Control's Source

The final step is to get a list of ProductPhoto objects, stuff those in session, and associate a dynamic path with the ImageUrl (or src) attribute of the image control. Typically, the ImageUrl (or src) attributes have been images; however, by using a page you effectively can obtain a dynamic palette on which to draw. Listing 5 shows a page that consumes the image loaded from a database, and Listing 6 shows the ASPX that binds a page path with the query string. Finally, the rendered images are shown in Figure 3.

Listing 5: A .aspx page that contains a GridView and the images loaded from the database.

Imports System.Web.Compilation
Imports System.Collections.Generic

Partial Class _Default
      Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.Load

      If (IsPostBack) Then Return
      Dim list As List(Of ProductPhoto) = _
         ReadProductPhoto.GetProductPhotos()
      Session("data") = list
      GridView1.DataSource = list
      GridView1.DataBind()

   End Sub

   Protected Function GetUrl(ByVal obj As Object) As String
      Dim o As ProductPhoto = CType(obj, ProductPhoto)
      Return String.Format("~/ProductImage.aspx?id={0}", _
                           o.ProductPhotoID)
   End Function

End Class

Listing 6: The HTML/ASPX that shows the GridView control and the binding statement that invokes the GetUrl method in Listing 5.

<%@ Page Language="VB" AutoEventWireup="false"
         CodeFile="Default.aspx.vb" Inherits="_Default" %>

<%@ Register Src="ImageControl.ascx" TagName="ImageControl"
             TagPrefix="uc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
   <title>Untitled Page</title>
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:GridView ID="GridView1" runat="server"
                    AutoGenerateColumns="False" Width="100%">
         <Columns>
            <asp:BoundField DataField="ProductPhotoID"
                            HeaderText="ID" />
            <asp:TemplateField HeaderText="Large Photo">
               <EditItemTemplate>
               </EditItemTemplate>
               <ItemTemplate>
                  <asp:Image ID="Image1" runat="server"
                     ImageUrl='<%# GetUrl(Container.DataItem) %>' />
               </ItemTemplate>
            </asp:TemplateField>
         </Columns>
      </asp:GridView>
      &nbsp;

   </div>
   </form>
</body>
</html>

Figure 3: The AdventureWorks2000 product photos rendered from the database.

Deriving Additional Benefits

So, now you know you can store and render images right from a database. This capability needs to be componentized (by Microsoft). An additional benefit is that, because you are rendering the database images from code, you can draw, paint, annotate, scale, rotate (or whatever you want to do to) the images because you effectively have a canvas (hDC, DC, or Graphics object, whichever term you are familiar with) to draw on.

Listing 7 demonstrates how you can scale the image, maintaining the aspect ratio by setting the largest side (height or width) to the new desired maximum size and then scaling the other dimension proportionately. Listing 8 shows how you can write on the image, supporting the idea of dynamic annotation, perhaps showing a sales price.

Listing 7: Code that scales the images (but keep in mind that the GIF images didn't render well after manipulation; the fault may be mine though).

Const max  As Integer = 64
Dim width  As Integer
Dim height As Integer
Dim scalar As Single

If (FImage.Width > FImage.Height) Then
   width  = max
   scalar = width / FImage.Width
   height = scalar * FImage.Height
Else
   height = max
   scalar = height / FImage.Height
   width  = scalar * FImage.Width
End If

Dim bmp As Bitmap = New Bitmap(width, height)
Dim g As Graphics = Graphics.FromImage(bmp)
g.DrawImage(FImage, 0, 0, bmp.Width, bmp.Height)
Dim stream As MemoryStream = New MemoryStream()
bmp.Save(stream, ImageFormat.Gif)
Response.BinaryWrite(stream.ToArray())

Listing 8: You have a perfectly valid canvas to write on, so you can use and of the GDI+ capabilities when you manage loading the images from the database.

Dim b As Bitmap   = New Bitmap(FImage.Width, FImage.Height)
Dim g As Graphics = Graphics.FromImage(b)
g.DrawImage(FImage, 0, 0)
Dim f As Font = New Font(FontFamily.GenericSansSerif, 12, _
                         FontStyle.Italic)
g.DrawString("Extra Information", f, Brushes.Red, 10, 10)
Dim stream As MemoryStream = New MemoryStream()
b.Save(stream, ImageFormat.Gif)
Response.BinaryWrite(stream.ToArray())

Finally, it is worth noting that you could store the path to the image in the database, use Image.LoadFromStream—Image.LoadFromFile locks images—and manipulate the image. Then, you could use the techniques described in this article to render the image. The main premise is why should images be stored differently when they don't have to be?

Summary

This article demonstrates how to store and retrieve images from a SQL Server database instead of storing the path and binding the URL. The technique renders the image to a GDI+ object and stuffs the results in the HttpResponse stream. Then, instead of setting the Image.ImageUrl (or <img> tag's src attribute) to the path, you assign the relevant attribute to the .aspx page containing the rendered image. The result is that you now store images in a way that is homogenous with other kinds of data, and you are free to manipulate the image—by scaling, rotating, annotating, for example—any way that images in general can be manipulated by GDI+.

Final Word: If you want an excellent read on general business information, read Geoffrey A. Moore's Dealing With Darwin: How Great Companies Innovate at Every Phase of Their Evolution.

About the Author

Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his new book UML DeMystified from McGraw-Hill/Osborne. Paul is a software architect for Tri-State Hospital Supply Corporation. You may contact him for technology questions at pkimmel@softconcepts.com.

If you are interested in joining or sponsoring a .NET Users Group, check out www.glugnet.org.

Copyright © 2007 by Paul Kimmel pkimmel@softconcepts.com. All Rights Reserved.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Where the business performance of their mobile app portfolios are concerned, most companies are flying blind. While traditional application portfolios are held to all kinds of ROI measure, the investment plan for mobile apps -- increasingly the more crucial bet -- is made by guesswork and dart-throwing. This interactive e-book investigates how mobile is driving the need for app and portfolio measures unlike any we saw in the days of web. Good mobile analytics must deliver leading indicators of user experience …

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds