Introduction
A while ago I needed to store some images within a database as part of a project. For a second task I also wanted to create thumbnails of the stored image data at varying sizes for an ASP.NET application that was part of my overall project.
This article will demonstrate how to store and retrieve image data from an SQL database, and shall also demonstrate how to create thumbnail images of this SQL database stored image data.
The Database
Why should we use a database and not just have the images within a virtual folder under the main ASP.NET folder? Well there are several reasons why it might be good to store images in a database, such as
- If the images binary data is stored in a database table, we have all the data required to make the image any size we want, and it will always look like the orginal image
- If the images binary data is stored in a database table, when we back the database up, we have also backed up all the users images
I am not saying this is the only way to go, its simply one way. As I say its something I had to do for a project, so I thought I would share what I found with you lot.
So what does the database look like.
It simply contains one table called tbl_image
which can be setup using the following script (contained within the object creation script at the top of this article)
CREATE TABLE [dbo].[tbl_image] (
[img_pk] [int] IDENTITY (1, 1) NOT NULL ,
[img_data] [image] NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
As can be seen, its a very simply table. We simply store the images primary key (img_pk
) as an int type column, and the image data (img_data
) as an image type column. This is all fairly easy isn't it?
So how do we get data in / out of this table. I have actually written three stored procedures (contained within the object creation script at the top of this article), but most of this could actually be done with standard SQL command strings. That I'll leave up to you.
Let's have a look at the three stored procedures shall we, they are quite easy, in fact I think the comments at the top pretty much explain them in enough detail. So I won't bore you with any more explanatory words, as it's clear, isn't it?
sp_AddImage
CREATE PROCEDURE [sp_AddImage]
(
@img_pk INT OUTPUT ,
@img_data IMAGE
)
AS
INSERT INTO tbl_Image ( img_data)
VALUES ( @img_data )
IF @@ROWCOUNT=1
SET @img_pk=@@IDENTITY
GO
sp_GetImageByID
CREATE PROCEDURE [sp_GetImageByID]
(
@img_pk INT
)
AS
SELECT img_data FROM tbl_Image
WHERE img_pk=@img_pk
GO
sp_GetImages
CREATE PROCEDURE [sp_GetImages]
AS
SELECT * FROM tbl_Image
GO
The Web Application
Design
To properly understand how this all works let's consider the following class diagram which should help.
Let's have a quick look at the design of the web application, before diving into how it all works, shall we?
Default is a standard ASP.NET wb form, and it looks like the following image in design time. This is the only page within the application.
It can be seen that this page simpy provides an upload field, and a button to submit the uploaded file.
Below that it has a literal element in which I use to show error messages (NOTE: This is not how to normally do error handling in ASP.NET, normally one would have a custom error page, but that is outside the scope of this article. This is simply to show the Exception message as quickly as possible).
Then there is a Placeholder element, which is used to add a thumbnail of the uploaded image (if sucessful).
Then there is a button to SELECT
all (using stored procedure sp_GetImages
) previously stored database images. These stored images are then bound to a DataList control. This is explained in more detail below.
That's the design of the page, but how does it all work?
So How Does It All Work
Well let's start at the beginning shall we? The following steps are carried out.
To upload and store an image
- The user selects a file from their local file system, using the upload field shown
- They then click the "Upload" button, which then checked the posted content, to see if is is actually an image. If it is valid image go to step 3, otherwise show an error message
- Create an image from the upload stream, and then convert this image to a
byte[]
array, and save this to the database using the dbAccess
class' SaveImageToDb()
method, which in turn calls the sp_AddImage
stored procedure above - If the save was successful, create a thumbnail of the image using the primary key of the current image.
To create a thumnail for an image
It is a little known fact that an image may be created as the result of writing another ASP.NET form (with no controls of its own) to the HTTP response stream. This is how we manage to do the thumbnails. Let's have a look shall we?
- If a primary key for an image is known, we can create thumbnail as shown in the following code snippet:
ThumbFromID.THUMBNAIL_SIZE = this.THUMBNAIL_SIZE;
ThumbFromID.USE_SIZE_FOR_HEIGHT = false;
imgImage = new System.Web.UI.WebControls.Image();
imgImage.ImageUrl = ThumbFromID.PAGE_NAME + "?" +
ThumbFromID.IMAGE_ID + "=" + img_pk;
What's actually going on here, is that we are setting two static field values within the ThumbFromID
class, which will dictate what size the thumbnail will be. Next we assign the current images ImageUrl
property the value of the string ThumbFromID.PAGE_NAME?ThumbFromID.IMAGE_ID=5;
for example. What this is doing is requesting the page ThumbFromID
to load. Remember that the page ThumbFromID
does not have any controls of its own, its sole job is to provide the correct image from the database at the requested size, on the standard HTTP response stream. This will probably make a bit more sense if we have a look at the Page_Load
event of the ThumbFromID
class.
private void Page_Load(object sender, System.EventArgs e)
{
byte[] imageData = null;
MemoryStream ms = null;
System.Drawing.Image fullsizeImage = null;
String imageID = null;
if (!Page.IsPostBack)
{
try
{
imageID = Request.QueryString[IMAGE_ID];
imageData = dbAccess.GetImageByID(int.Parse(imageID));
ms = new MemoryStream(imageData);
fullsizeImage = System.Drawing.Image.FromStream(ms);
Response.ContentType = "image/Jpeg";
ImageResize ir = new ImageResize();
ir.File = fullsizeImage;
if (USE_SIZE_FOR_HEIGHT)
ir.Height = THUMBNAIL_SIZE;
else
ir.Width = THUMBNAIL_SIZE;
ir.GetThumbnail().Save(Response.OutputStream,
System.Drawing.Imaging.ImageFormat.Jpeg);
}
catch (Exception ex)
{
throw new Exception(ex.Message, ex);
}
finally
{
ms.Close();
}
}
}
Can you see what is happening here? A couple of fields are created to hold the finished image and a MemoryStream
is created for temporary storage. Then the value of the images primary key is read from the request string (this would be 5 if we stick to the example above). So next the image data is fetched from the dbAccess
class by using the GetImageByID(..)
method (which in turn calls the stored procedure sp_GetImageByID
). This binary image data from the database is then placed into the MemoryStream
object. Then a new ImageRezize
object is created, and is used to create a thumbnail at the requested size. The ImageRezize
class is not my own, it is by a chap called Alex Hildyard, and is available at here
The ImageRezize
object, simply resizes the image to the correct size, and writes the image to a stream. In this case, as we are dealing with ASP.NET, and are trying to create a image control ImageUrl
property, this is set to Response.OutputStream
How about doing this with a databound control
You may want to use thumbnail images within a databound control, such as a DataList or GridView, etc. To demonstrate this let's consider the following method of the Default
page, which contains a DataList
control. As shown previously in the Design section of this article.
Well, let's look at that shall we, The Default
page provides a single button called btnSeeAll, whos job it is to SELECT all the SQL stored images by using the dbAccess
classes GetImages(..) method (which in turn calls the stored procedure sp_GetImages)
protected void dlImages_ItemDataBound(object sender, DataListItemEventArgs e)
{
System.Web.UI.WebControls.Image img = null;
if ((e.Item.ItemType == ListItemType.Item) ||
(e.Item.ItemType == ListItemType.AlternatingItem))
{
img = (System.Web.UI.WebControls.Image)
(e.Item.FindControl("imgThumbnail"));
int img_pk = Convert.ToInt16(
((DataRowView)e.Item.DataItem).Row.ItemArray[0].ToString());
ThumbFromID.THUMBNAIL_SIZE = this.THUMBNAIL_SIZE;
ThumbFromID.USE_SIZE_FOR_HEIGHT = false;
img.ImageUrl = ThumbFromID.PAGE_NAME + "?" +
ThumbFromID.IMAGE_ID + "=" + img_pk;
}
}
It can be seen that this to follows the same principles as just outlined. The only difference being that the dlImages_ItemDataBound
event is raised during databinding, for each item within the databound control (A DataList in this case). As such the extraction of the primary key must be gained by examining the DataSet that is being bound to the databound control. However, once we have the current DataRowView
Primary key value, which is obtained thusly:
Convert.ToInt16(((DataRowView)e.Item.DataItem).Row.ItemArray[0].ToString())
We simpy create the thumnail the same way. This is repeated for each
DataRowView
within the DataLists data source.
Full Code Listing
For those of you that want full code listings.
dbAccess
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using System.IO;
#region dbAccess CLASS
public sealed class dbAccess
{
public static byte[] GetImageByID(int img_pk)
{
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[
"EasyThumbs_DB"].ConnectionString))
{
try
{
SqlCommand command = new SqlCommand("sp_GetImageByID",
connection);
command.CommandType = CommandType.StoredProcedure;
SqlParameter param0 = new SqlParameter(
"@img_pk", SqlDbType.Int);
param0.Value = img_pk;
command.Parameters.Add(param0);
connection.Open();
byte[] imgdata = (byte[])(command.ExecuteScalar());
connection.Close();
return imgdata;
}
catch (SqlException ex)
{
throw new Exception(ex.Message, ex);
}
}
}
public static int SaveImageToDB(ref int img_pk, byte[] imgbin)
{
try
{
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[
"EasyThumbs_DB"].ConnectionString))
{
SqlCommand command = new SqlCommand("sp_AddImage",
connection);
command.CommandType = CommandType.StoredProcedure;
int IMG_PK = 0;
SqlParameter param0 = new SqlParameter("@img_pk",
SqlDbType.Int);
param0.Value = IMG_PK;
command.Parameters.Add(param0);
param0.Direction = ParameterDirection.Output;
SqlParameter param1 = new SqlParameter("@img_data",
SqlDbType.Image);
param1.Value = imgbin;
command.Parameters.Add(param1);
connection.Open();
int numRowsAffected = command.ExecuteNonQuery();
connection.Close();
img_pk = int.Parse(param0.Value.ToString());
return numRowsAffected;
}
}
catch (SqlException ex)
{
throw new Exception(ex.Message, ex);
}
}
public static DataSet GetImages()
{
using (SqlConnection connection = new SqlConnection(
ConfigurationManager.ConnectionStrings[
"EasyThumbs_DB"].ConnectionString))
{
try
{
SqlCommand command = new SqlCommand("sp_GetImages",
connection);
command.CommandType = CommandType.StoredProcedure;
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = command;
DataSet ds = new DataSet();
da.Fill(ds, "IMAGES");
return ds;
}
catch (SqlException ex)
{
throw new Exception(ex.Message, ex);
}
}
}
}
#endregion
ImageResize
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Drawing;
using System.IO;
#region ImageResize CLASS
public class ImageResize
{
#region Instance Fields
private double m_width, m_height;
private bool m_use_aspect = true;
private bool m_use_percentage = false;
private System.Drawing.Image m_src_image, m_dst_image;
private System.Drawing.Image m_image;
private ImageResize m_cache;
private Graphics m_graphics;
#endregion
#region Public properties
public System.Drawing.Image File
{
get { return m_image; }
set { m_image = value; }
}
public System.Drawing.Image Image
{
get { return m_src_image; }
set { m_src_image = value; }
}
public bool PreserveAspectRatio
{
get { return m_use_aspect; }
set { m_use_aspect = value; }
}
public bool UsePercentages
{
get { return m_use_percentage; }
set { m_use_percentage = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public double Height
{
get { return m_height; }
set { m_height = value; }
}
#endregion
#region Public Methods
public virtual System.Drawing.Image GetThumbnail()
{
bool recalculate = false;
double new_width = Width;
double new_height = Height;
if (m_src_image != null)
m_src_image.Dispose();
m_src_image = m_image;
recalculate = true;
if (UsePercentages)
{
if (Width != 0)
{
new_width = (double)m_src_image.Width * Width / 100;
if (PreserveAspectRatio)
{
new_height = new_width * m_src_image.Height /
(double)m_src_image.Width;
}
}
if (Height != 0)
{
new_height = (double)m_src_image.Height * Height / 100;
if (PreserveAspectRatio)
{
new_width = new_height * m_src_image.Width /
(double)m_src_image.Height;
}
}
}
else
{
if (PreserveAspectRatio)
{
if (Width != 0 && Height == 0)
{
new_height = (Width / (
double)m_src_image.Width) * m_src_image.Height;
}
else if (Height != 0 && Width == 0)
{
new_width = (Height / (
double)m_src_image.Height) * m_src_image.Width;
}
}
}
recalculate = true;
if (recalculate)
{
if (m_dst_image != null)
{
m_dst_image.Dispose();
m_graphics.Dispose();
}
Bitmap bitmap = new Bitmap((int)new_width, (int)new_height,
m_src_image.PixelFormat);
m_graphics = Graphics.FromImage(bitmap);
m_graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.HighQuality;
m_graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
m_graphics.DrawImage(
m_src_image, 0, 0, bitmap.Width, bitmap.Height);
m_dst_image = bitmap;
m_cache = this.MemberwiseClone() as ImageResize;
}
return m_dst_image;
}
#endregion
#region Deconstructor
~ImageResize()
{
if (m_dst_image != null)
{
m_dst_image.Dispose();
m_graphics.Dispose();
}
if (m_src_image != null)
m_src_image.Dispose();
}
#endregion
}
#endregion
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs"
Inherits="_Default" %>
<!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:Label ID="lblInstructions" runat="server" Font-Bold="True"
Font-Names="Arial" Text="Please select an image file"></asp:Label>
<br/>
<asp:FileUpload ID="UploadFile" runat="server" Width="631px" />
<asp:Button ID="btnSubmit" runat="server" Height="20px"
OnClick="btnSubmit_Click" Text="Upload" Width="78px" />
<br/>
<br />
<asp:Label ID="lblException" runat="server" Font-Bold="True"
Font-Names="Arial" Text="ERRORS : If any exist"
ForeColor="Red"></asp:Label>
<br />
<asp:Literal ID="litException" runat="server"></asp:Literal>
<br/>
<br />
<asp:PlaceHolder id="plImage" Runat="server"></asp:PlaceHolder>
<br />
<br />
<asp:Label ID="lblSeeAll" runat="server" Font-Bold="True"
Font-Names="Arial"
Text="View all saved images from SQL database"></asp:Label>
<br />
<br />
<asp:Button ID="btnSeeAll" runat="server" Height="20px"
Text="See All Images" Width="115px" OnClick="btnSeeAll_Click" />
<br />
<br />
<asp:datalist id="dlImages" Runat="server" Width="90%"
ItemStyle-HorizontalAlign="Left" RepeatLayout="Table"
RepeatDirection="Horizontal" RepeatColumns="4"
OnItemDataBound="dlImages_ItemDataBound">
<ItemTemplate>
<asp:Image id="imgThumbnail" runat="server" borderSize="5" />
</ItemTemplate>
</asp:datalist>
<br/>
</div>
</form>
</body>
</html>
Default.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
public partial class _Default : System.Web.UI.Page
{
#region instance fields
protected System.Web.UI.WebControls.Image imgImage;
private int THUMBNAIL_SIZE = 60;
#endregion
protected void Page_Load(object sender, EventArgs e)
{
}
private void createImageFromDBBytes(int img_pk)
{
ThumbFromID.THUMBNAIL_SIZE = this.THUMBNAIL_SIZE;
ThumbFromID.USE_SIZE_FOR_HEIGHT = false;
imgImage = new System.Web.UI.WebControls.Image();
imgImage.ImageUrl = ThumbFromID.PAGE_NAME + "?" +
ThumbFromID.IMAGE_ID + "=" + img_pk;
litException.Visible = false;
plImage.Controls.Clear();
plImage.Controls.Add(imgImage);
}
private byte[] BmpToBytes(System.Drawing.Image bmp)
{
MemoryStream ms = null;
byte[] bmpBytes = null;
try
{
ms = new MemoryStream();
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
bmpBytes = ms.GetBuffer();
}
catch (Exception ex)
{
return null;
}
finally
{
bmp.Dispose();
if (ms != null)
{
ms.Close();
}
}
return bmpBytes;
}
protected void btnSubmit_Click(object sender, EventArgs e)
{
try
{
string imgContentType = UploadFile.PostedFile.ContentType;
if (imgContentType.ToLower().StartsWith("image"))
{
System.Drawing.Bitmap b = (
System.Drawing.Bitmap)System.Drawing.Image.FromStream(
UploadFile.PostedFile.InputStream);
int img_pk = 0;
int RowsAffected = dbAccess.SaveImageToDB(ref img_pk,
BmpToBytes(b));
if (RowsAffected > 0)
{
createImageFromDBBytes(img_pk);
}
else
{
litException.Text = ("<br><p>ERROR saving image </p>");
}
}
else
{
litException.Text = ("<br><p>The file is not an image</p>");
}
}
catch (Exception ex)
{
litException.Text = ("<br><p>" + ex.Message + "</p>");
}
}
protected void btnSeeAll_Click(object sender, EventArgs e)
{
try
{
DataSet dsImgs = dbAccess.GetImages();
dlImages.DataSource = dsImgs;
dlImages.DataBind();
}
catch (Exception ex)
{
litException.Text = ("<br><p>" + ex.Message + "</p>");
}
}
protected void dlImages_ItemDataBound(object sender,
DataListItemEventArgs e)
{
System.Web.UI.WebControls.Image img = null;
if ((e.Item.ItemType == ListItemType.Item) ||
(e.Item.ItemType == ListItemType.AlternatingItem))
{
img = (System.Web.UI.WebControls.Image)
(e.Item.FindControl("imgThumbnail"));
int img_pk = Convert.ToInt16(
((DataRowView)e.Item.DataItem).Row.ItemArray[0].ToString());
ThumbFromID.THUMBNAIL_SIZE = this.THUMBNAIL_SIZE;
ThumbFromID.USE_SIZE_FOR_HEIGHT = false;
img.ImageUrl = ThumbFromID.PAGE_NAME + "?" +
ThumbFromID.IMAGE_ID + "=" + img_pk;
}
}
}
ThumbFromID.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ThumbFromID.aspx.cs"
Inherits="ThumbFromID" %>
ThumbFromID.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Data.OleDb;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
#region ThumbFromID CLASS
public partial class ThumbFromID : System.Web.UI.Page
{
#region Instance Fields
public const String PAGE_NAME = "ThumbFromID.aspx";
public const String IMAGE_ID = "img_pk";
public static int THUMBNAIL_SIZE;
public static bool USE_SIZE_FOR_HEIGHT;
#endregion
#region Private Methods
private void Page_Load(object sender, System.EventArgs e)
{
byte[] imageData = null;
MemoryStream ms = null;
System.Drawing.Image fullsizeImage = null;
String imageID = null;
if (!Page.IsPostBack)
{
try
{
imageID = Request.QueryString[IMAGE_ID];
imageData = dbAccess.GetImageByID(int.Parse(imageID));
ms = new MemoryStream(imageData);
fullsizeImage = System.Drawing.Image.FromStream(ms);
Response.ContentType = "image/Jpeg";
ImageResize ir = new ImageResize();
ir.File = fullsizeImage;
if (USE_SIZE_FOR_HEIGHT)
ir.Height = THUMBNAIL_SIZE;
else
ir.Width = THUMBNAIL_SIZE;
ir.GetThumbnail().Save(Response.OutputStream,
System.Drawing.Imaging.ImageFormat.Jpeg);
}
catch (Exception ex)
{
throw new Exception(ex.Message, ex);
}
finally
{
ms.Close();
}
}
}
#endregion
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
#endregion
Web.Config
<?xml version="1.0"?>
<!--
Note: As an alternative to hand editing this file you can use the
web admin tool to configure settings for your application. Use
the Website->Asp.Net Configuration option in Visual Studio.
A full list of settings and comments can be found in
machine.config.comments usually located in
\Windows\Microsoft.Net\Framework\v2.x\Config
-->
<configuration>
<appSettings/>
<connectionStrings>
<add name="EasyThumbs_DB"
connectionString="server=localhost;uid=sa;pwd=sa;Database=EasyThumbs"/>
</connectionStrings>
<system.web>
<!--
Set compilation debug="true" to insert debugging
symbols into the compiled page. Because this
affects performance, set this value to true only
during development.
-->
<compilation debug="true"/>
<!--
The <authentication> section enables configuration
of the security authentication mode used by
ASP.NET to identify an incoming user.
-->
<authentication mode="Windows"/>
<!--
The <customErrors> section enables configuration
of what to do if/when an unhandled error occurs
during the execution of a request. Specifically,
it enables developers to configure html error pages
to be displayed in place of a error stack trace.
<customErrors mode="RemoteOnly"
defaultRedirect="GenericErrorPage.htm">
<error statusCode="403" redirect="NoAccess.htm" />
<error statusCode="404" redirect="FileNotFound.htm" />
</customErrors>
-->
</system.web>
</configuration>
What Do You Think ?
That's it, I would just like to ask, if you liked the article please vote for it.
Conclusion
I hope this has not been to hard for most folks to follow. I think its a fairly easy thumbnail solution.
History
v1.0 06/01/07