Rendering Images in ASP.NET Directly from Your Database
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.
Listing 1: The Data Access layer that constructs the ProductPhoto generic list. 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. 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. 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. 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
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
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
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
Rendering an Image to 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
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.)
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
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>
</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?
No comments:
Post a Comment