Application Security Testing: An Integral Part of DevOps
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
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.