A screenshot
Introduction
Welcome to my first article in CodeProject! The picture above should give an idea
of what the code i developed does. I started this project
after reading the (very good) ASP.NET tutorials of the Framework SDK. The DataList control
gave me the idea to make a user control to display thumbnails in grid format. In
order to generate thumbnails 'on the fly' i made a C# class and a C# HTTP
handler. Part of the solution (although optional) is an ATL COM
thumbnail-generator object. I have added useful features like paging and beveled
thumbnails. After the first version of this article some people asked for
comments below the images. In this second version i haved added comments and
online editing of them. Since i don't have the VS.NET, developing was made
using the free Web
Matrix tool. It is a very nice tool with many design features, unfortunately the code editor is not
so good yet.
Following, I will describe how to use the thumbnail tools and then
(briefly) important parts of the code.
How to use the tools
The ASP.NET part of my thumbnail solution consists of the files PhilipSoft.Thumbnails.ThumbGenerator.cs,
ThumbJpeg.ashx
and ThumbList.ascx. The web.config file
contains some application settings (appSettings), however all of them are optional.
After downloading the zip and extracting the files,
make the containing ThumbAspnet folder a virtual directory and point the
browser to the ThumbDisplay.aspx page. This is a test page, an
instance of which is shown at the figure above. Enter in the Path textbox a path
of an IIS virtual-directory containing images and press the
'Regenerate thumbnails' button. You should see thumbnails of images residing in
that directory. Try the
other choices also and see the changes on the thumbnails. If you want the 'Use
COM object' choise to work, you must register the ThumbExtract.dll found in
the bin subfolder. More on this COM object later.
ThumbJpeg.ashx
is an .NET-based HTTP handler that you can use in an <img>
tag to produce a thumbnail dynamically.
The thumbnail
is not saved in disk but it is put in cache memory to be available for
subsequent calls. If you change a parameter it will be generated again and not be taken from
the cache. A typical use of the handler is shown below.
The <a>
link points to an image and the <img>
tag specifies a dynamically generated thumbnail via its src
attribute.
<a href='/images/OLIMP012.jpg'>
<img src='ThumbJpeg.ashx?VFilePath=/images/OLIMP012.jpg&width=200&
height=200&Bevel=true'
alt='Click here'/></a>
Parameters to the HTTP handler are given in the form of a query string.
They are the following:
- VFilePath: Virtual path of original image. The only mandatory parameter.
If the file specified does not exist or it is not an image, a "no
thumbnail" default image is generated from the NoThumb.gif file.
- Width, Height: desired thumbnail size. If not given it is read from
appSettings, if not specified in appSettings defaults to 150x150.
- AllowStretch: Set to 'true' to allow stretching of thumbnail to fit the
above size. If not given defaults to false.
- Bevel: Set to 'true' to generate a beveled thumbnail, as shown in the
image above. If not given defaults to false.
- Refresh: Set to 'true' to refresh thumbnail (erase the cache version
first). I've not used it and it is rather useless.
- UseCOMobj: Set to 'true' to generate the thumbnail using the COM object
implemented in ThumbExtract.dll (you must register it first). The ProgID for
the COM object is 'ThumbExtract.FileThumbExtract'
- ShowComments: Set to 'true' to show a comment (if exists) under the
thumbnail. Comments are saved in an XML file named 'ThumbComments.xml'
located in the same directory as the images.
- AllowEdit: If it is set to 'true' an edit link allows you to edit the
comment for each thumbnail. Then you press 'update' and the comment is saved
in the XML file.
A reason to use the COM object (by setting UseCOMobj property to 'true'), is its capability to generate thumbnails
for more file types than the C# code. It exploits the shell
interface (IExtractImage) responsible for generating thumbnails when you
click a file or select the Thumbnail view in Explorer. For example
see the image below. The object generated thumbnails for a DICOM
(medical) image, an HTML file, a Powerpoint file and a Scribble (Visual C++
tutorial) file (drawing). I haved submited another article (Create
Thumbnail Extractor objects for your MFC document types) about how to develop a COM object
that can extract thumbnails for Scribbles and generally any file type by
implementing the IExtractImage interface. In the zip download for that
article you can find the source code for the COM component ThumbExtract.dll. In
this article's zip you can find only the binary file.
ThumbList.ascx implements a user control (ThumbList
)
based on the DataList control that simplifies the creation of Web thumbnail
views. Here are the properties of
the ThumbList
user control:
- VPath: Virtual directory that you want to display its contents as
thumbnails
- Filter: Filter to select the files for which you want to display
thumbnails (e.g. *.jpg;*.gif)
- Width, Height,AllowStretch,Bevel,Refresh,UseCOMobj: see explanations above
- Columns: number of columns of the view
- HeaderTitle: title appearing at the header of the control
- Sort: Set to true to sort by file name
- AllowPaging,PageSize: Set AllowPaging=true to show pages containing
PageSize
thumbnails each
- CurPage: Set to a value above 0 to define a current page. Normally you don't set
this value in order to let user to define the current page by clicking on the page links
at the footer of the control.
- HideNavBar: Hide navigation bar located at the footer of the control
- SaveThumbnails: Save every thumbnail (if not previously saved) to the hard
disk and point to the saved thumbnail (and not to the HTTP handler). The
thumbnail files will be saved in a 'thumbnail' subfolder below the folder
corresponding to the virtual directory. This is a good option if you want to
use the control in a busy Web site because it will save CPU time and
increase resposiveness of the server (however if you let the user to choose
between various thumbnail sizes then many small files will be created in
your hard disk). Additionally, if your Web hosting provider does not
support ASP.NET pages you can run a web spider program in your test server
to create static HTML pages.
If you change the location of ThumbList.ascx (e.g. put it in a
subfolder), you must put ThumbJpeg.ashx file in the same location. The
ThumbDisplay.aspx page contains an instance of the control and
manipulates its properties based on user selections.
Thumbnail generator class
The core of this thumbnail solution is the PhilipSoft.Thumbnails.ThumbGenerator.cs class. I
encapsulated generation of thumbnails into a class since i can call it from many
ASP.net pages and also from Windows Forms pages (i haven't tried this yet).
After creating an instance, you set the thumbnail parameters by calling its SetParams(...)
function. Then, you can get the thumbnail by calling the ExtractThumbnail()
function which returns a Bitmap
corresponding to the
thumbnail. The GetUniqueThumbName()
function returns a unique
filename with respect to the parameters of the thumbnail. The code below is
a part of the ExtractThumbnail()
function that creates the thumbnail using the GetThumbnailImage
of the Bitmap
class. In order to retain the aspect ratio of the
original image, i use the same subsampling factor f
to derive the actual
dimensions of the generated Bitmap
.
bitmapNew = new Bitmap(_path);
if(!_bStretch) {
widthOrig = bitmapNew.Width;
heightOrig = bitmapNew.Height;
fx = widthOrig/_width;
fy = heightOrig/_height;
f=Math.Max(fx,fy); if(f<1) f=1;
widthTh = (int)(widthOrig/f); heightTh = (int)(heightOrig/f);
}
else {
widthTh = _width; heightTh = _height;
}
bitmapNew = (Bitmap)bitmapNew.GetThumbnailImage(widthTh, heightTh,
new Image.GetThumbnailImageAbort(ThumbnailCallback),IntPtr.Zero);
if(!_bBevel) return bitmapNew;
}
public bool ThumbnailCallback() { return false; }
If you choose to create the thumbnail using the COM object, the following code
is executed:
if(_oCOMThumb==null) _oCOMThumb = new FileThumbExtract();
_oCOMThumb.SetPath(_path);
_oCOMThumb.SetThumbnailSize(_width,_height);
IntPtr hBitmap = (IntPtr)_oCOMThumb.ExtractThumbnail();
bitmapNew = Bitmap.FromHbitmap(hBitmap);
_oCOMThumb.FreeThumbnail();
FileThumbExtract
class is a wrapper class for the COM object,
generated using the tlbimp.exe
NET framework tool. I will not
discuss its code, it is based on a sample
from the GotDotNet site. The ExtractThumbnail()
returns an HBITMAP
handle (as a long value)
which is used in the Bitmap.FromHbitmap
static function to create
the thumbnail (FromHbitmap
function comes from the base Image
class, unfortunately by mistake it is not listed in the documentation of the Bitmap
class). An nice feature of the COM object is that it can generate thumbnails for
URLs also !
Applying the bevel effect on the generated thumbnail was an interesting
issue. After observing how Photoshop applies the effect i realized that i should
draw at the borders of the thumbnail linear gradients with decreasing
transparency (increasing A color value). C# GDI+ offers many capabilities and
its use is much simpler than normal C GDI, so it was not difficult to implement
the effect. See the code below (some lines omitted) for the implementation, may
be you can use it to create a Web or Forms beveled button that draws itself
based on parameters. The bevel effect it creates is not perfect but it is
satisfactory. Parameters of the effect (like bevel width) are fixed but you
could make them adjustable.
int widTh,heTh;
widTh = bitmapNew.Width; heTh = bitmapNew.Height;
int BevW = 10, LowA=0, HighA=180, Dark=80, Light=255;
Color clrHi1 = Color.FromArgb(LowA,Light,Light,Light);
Color clrHi2 = Color.FromArgb(HighA,Light,Light,Light);
Color clrDark1 = Color.FromArgb(LowA,Dark,Dark,Dark);
Color clrDark2 = Color.FromArgb(HighA,Dark,Dark,Dark);
LinearGradientBrush br; Rectangle rectSide;
Graphics newG = Graphics.FromImage(bitmapNew);
Size szHorz = new Size(widTh,BevW);
Size szVert = new Size(BevW,heTh);
szHorz+=new Size(0,2); szVert+=new Size(2,0);
rectSide = new Rectangle(new Point(0,heTh-BevW),szHorz);
br = new LinearGradientBrush(
rectSide,clrDark1,clrDark2,LinearGradientMode.Vertical);
rectSide.Inflate(0,-1);
newG.FillRectangle(br,rectSide);
...
...
...
...
br.Dispose(); newG.Dispose();
return bitmapNew;
Thumbnail HTTP handler
The ThumbJpeg.ashx
file HTTP handler is responsible for sending the thumbnail
to the output. Before creating it, it checks if a thumbnail created with the same
parameters already exists in the cache. To enable this, cached thumbnails are
associated with a key returned by the GetUniqueThumbName()
function. If it exists, it retrieves it using the key, otherwise it uses the services of the ThumbGenerator
class
to get the thumbnail. Finally the thumbnail is sent to the response as a JPEG file:
_oGenerator.SetParams(_path,_width,_height,_bStretch,_bBevel,_
bUseCOMobject);
Cache MyCache = context.Cache;
sCacheKey = _oGenerator.GetUniqueThumbName();
bool bRefresh = (context.Request["Refresh"]=="true");
if(bRefresh) MyCache.Remove(sCacheKey);
if(MyCache[sCacheKey] == null)
{
try {
bitmap = _oGenerator.ExtractThumbnail();
bFoundInCache=false;
}
catch(Exception e) {
...
}
}
else bitmap = (Bitmap)MyCache[sCacheKey];
context.Response.ContentType = "image/Jpeg";
bitmap.Save (context.Response.OutputStream, ImageFormat.Jpeg);
If the thumbnail bitmap is not found in the application cache, it is added to
it using the Insert
function. A dependency on the original image
file is created so that if the original image changes the cached bitmap becomes
obsolete and is removed from the cache. I have also set a sliding expiration of mins
minutes. Independently of these settings, the ASP.NET cache system periodically
removes objects from
the cache so you don't have to worry about memory usage.
if(!bFoundInCache) {
CacheDependency dependency = new CacheDependency(_path);
int mins; try {
mins = int.Parse(ConfigurationSettings.AppSettings["SlidingExpireMinutes"]);
} catch(ArgumentNullException ex) { mins=20; }
MyCache.Insert(sCacheKey, bitmap ,dependency,
Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(mins),
CacheItemPriority.Default, new CacheItemRemovedCallback(RemovedCallback));
dependency.Dispose();
}
}
static public void RemovedCallback(String k, Object item,
CacheItemRemovedReason r) {
((Bitmap)item).Dispose();
}
ThumbList User Control
The user control implemented in ThumbList.ascx
displays thumbnail
views. It is based on an instance of a DataList which is an ASP.NET templated data-bound
control. Creation of the data source and databinding is implemented in the
function BindThumbList()
listed below. As data source i use the DefaultView
of a DataTable
named dtThumbs created in memory. The table has two columns, the
Filename
and the Comment
. For each filename in the virtual directory (VPath) to be shown, a new row is added to the table. The
XML comments file is loaded as a DataSet
(using the ReadXml
function). If it does not exist, it is created in memory. Then, for each row in
the XML, a matching row is searched in the dtThumbs DataTable
to set the comment. In order to support paging, i had to write additional code
to select the files belonging to the each page. Unfortunately, DataList
does not inherently support paging. Since the
properties of the ThumbList
control may have changed by the calling
page, BindThumbList()
is executed at every page load, except when
the update command is executed.
public void BindThumbList()
{
String[] files; int i;
ArrayList arFilesAll = new ArrayList();
try {
if(_filter.Length==0 || _filter=="*.*") {
files = Directory.GetFiles(_path);
arFilesAll.AddRange(files);
}
else {
String[] filters = _filter.Split(';');
for(i=0; i<filters.Length; i++) {
files = Directory.GetFiles(_path,filters[i]);
arFilesAll.AddRange(files);
}
}
for(i=0; i<arFilesAll.Count; i++)
arFilesAll[i] = Path.GetFileName((String)arFilesAll[i]);
MyThumbList.Attributes["TotalFiles"] = arFilesAll.Count.ToString();
if(_bSaveThumbnails) {
_oGenerator = new ThumbGenerator();
String pathThumbs = _path+"\\thumbnails";
Directory.CreateDirectory(pathThumbs);
}
} catch(Exception exDir) { arFilesAll = new ArrayList(); }
if(Sort)
arFilesAll.Sort();
ArrayList arFilesToShow;
if(!AllowPaging)
arFilesToShow = arFilesAll;
else {
int totalPages = (int)Math.Ceiling((float)arFilesAll.Count/PageSize);
if(totalPages==0) totalPages=1;
MyThumbList.Attributes["TotalPages"] = totalPages.ToString();
if(CurPage<1) {
try {
CurPage = int.Parse(hdnCurPage.Value);
} catch(Exception exParse) { CurPage=1; }
}
if(hdnPrevPath.Value!=VPath)
CurPage = 1;
Trace.Warn("BindThumbList: Current page=" + CurPage.ToString());
if(CurPage>totalPages) CurPage=totalPages;
else if(CurPage<1) CurPage=1;
MyThumbList.Attributes["CurPage"] = CurPage.ToString();
hdnCurPage.Value = CurPage.ToString();
int startIndex = (CurPage-1)*PageSize;
int endIndex = Math.Min(CurPage*PageSize,arFilesAll.Count);
arFilesToShow = arFilesAll.GetRange(startIndex, endIndex-startIndex );
}
DataTable dtThumbs = CreateCommentsTable();
DataView dvCommentsFromXML = null;
if(ShowComments || AllowEdit) {
DataSet dsComments = ReadCommentsFromXML();
dvCommentsFromXML = dsComments.Tables[0].DefaultView;
}
for(i=0; i<arFilesToShow.Count; i++) {
DataRow dr = dtThumbs.NewRow();
dr[0] = arFilesToShow[i];
dtThumbs.Rows.Add(dr);
}
if((ShowComments || AllowEdit) && dvCommentsFromXML!=null) {
for(i = 0; i < dvCommentsFromXML.Count; i++) {
String sFilename=((String)dvCommentsFromXML[i][0]).Trim();
DataRow[] foundRows = dtThumbs.Select("Filename = '"+sFilename+"'");
if(foundRows.Length>0)
foundRows[0][1] = dvCommentsFromXML[i][1];
}
}
MyThumbList.DataSource = dtThumbs.DefaultView;
MyThumbList.DataBind();
}
When the Update button is pressed, the ThumbList_UpdateCommand
function is executed to update the XML file having the comments. By exploiting
the XML reading/writing capabilities of the DataSet, i treat the XML comments
file as a relational table and thus i can easily change my datasource to a
traditional database (e.g. an Access file). I chose the XML solution because it
is convenient for a small database and you can edit it by hand if necessary. You
can safely add HTML tags in the comments because when the file is saved the
'<' and '>' characters are escaped and thus the XML structure is not
broken. Note also that in the previous version of this thumbnail control i had
the DataList EnableViewState
property set to 'false' and this
prevented the update command to be executed. When i put the proporty to 'true'
the command was executed normally.
void ThumbList_UpdateCommand(Object sender, DataListCommandEventArgs e)
{
DataRowView drvTarget;
String sComment = (String)((TextBox)e.Item.FindControl("txtComment")).Text;
String sFilename = ((TextBox)e.Item.FindControl("txtFilename")).Text;
DataSet dsComments = ReadCommentsFromXML();
DataView dvCommentsFromXML = dsComments.Tables[0].DefaultView;
dvCommentsFromXML.RowFilter = "Filename='"+sFilename+"'";
if (dvCommentsFromXML.Count > 0) {
drvTarget = dvCommentsFromXML[0];
dvCommentsFromXML.RowFilter = "";
}
else {
drvTarget = dvCommentsFromXML.AddNew();
drvTarget[0] = sFilename;
}
drvTarget[1] = sComment;
drvTarget.EndEdit();
dsComments.WriteXml(_path +"\\ThumbComments.xml");
MyThumbList.EditItemIndex = -1;
BindThumbList();
}
DataSet ReadCommentsFromXML() {
DataSet dsComments = new DataSet("ThumbnailDataset");
try {
dsComments.ReadXml(_path +"\\ThumbComments.xml");
} catch(Exception exRead) { }
if(dsComments.Tables.Count==0)
dsComments.Tables.Add(CreateCommentsTable());
return dsComments;
}
DataTable CreateCommentsTable() {
DataTable dtThumbs = new DataTable("ThumbComment");
DataColumn dcPrimary = new DataColumn("Filename", typeof(string));
dtThumbs.Columns.Add(dcPrimary);
dtThumbs.Columns.Add(new DataColumn("Comment", typeof(string)));
dtThumbs.PrimaryKey = new DataColumn[] { dcPrimary };
dtThumbs.CaseSensitive = false;
return dtThumbs;
}
The item template specifies a thumbnail which links to the original image.
You can see that we have flexibility for databinding expressions. ThumbUrl
is a function that creates the link to the
thumbnail (the parametrised HTTP handler or the saved thumbnail if SaveThumbnails
is true). AltString
is a function that creates a string with the
filename and the size of the original image file.
<ItemTemplate>
<a href="<%# String.Format("{0}/{1}",_vpath,
((DataRowView)Container.DataItem)["Filename"]) %>" >
<img border="0"
src="<%# ThumbUrl((String)((DataRowView)Container.DataItem)["Filename"]) %>"
alt="<%# AltString((String)((DataRowView)Container.DataItem)["Filename"]) %>" />
</a>
<%# _bShowFilenames?"<br/>"+((DataRowView)Container.DataItem)["Filename"]:"" %>
<%# ShowComments?"<br/>"+((DataRowView)Container.DataItem)["Comment"]:"" %>
</br><asp:LinkButton id="button1" Visible='<%# AllowEdit ? true:false %>'
Text="Edit" CommandName="Edit" runat="server"/>
</ItemTemplate>
The trickiest part was how to implement the page links and respond to them. The
page links are created inside the DataList control footer with code executed in
the handler of the
DataBound
event. I chose that event
because at that time databinding has occured and we know how many pages we need.
At
first i created page links as
LinkButton
controls and defined
a common command handler. Inside the handler, the page number was determined
from the CommandEventArgs. However i had to databind again to show the page that
the user selected. I found a better solution to avoid the double databinding by
exploiting the
RegisterClientScriptBlock
function of the
Page
class. At page load a
GoToPage
client javascript function is added
(see the code below).
This function is executed by the page links. It sets the
hdnCurPage
hidden field's value equal to the argument n (page number). The
ClientID
property permits to access a server control from the client side! After postback
we can retrieve the page
number from the hidden field at page load (see also the
BindThumbList()
function above). The
GetPostBackEventReference
function of the
Page
class returns a reference to the postback function that resubmits the form. This is
important because if you create a simple link to the same page and the form is not resubmitted, the server controls lose their
state.
void Page_Load(Object Src, EventArgs e)
{
StringBuilder scriptb = new StringBuilder();
scriptb.Append("<script
language="\""javascript\">\n");
scriptb.Append("//<!--\n function GoToPage(vpath,n) { \n");
scriptb.AppendFormat("document._ctl0.{0}.value=n; \n",hdnCurPage.ClientID);
scriptb.Append(Page.GetPostBackEventReference(this));
scriptb.Append("\n } \n//-->\n </");
scriptb.Append("script>");
if(!Page.IsClientScriptBlockRegistered("clientGotoPageScript"))
Page.RegisterClientScriptBlock("clientGotoPageScript",scriptb.ToString());
if(MyThumbList.EditItemIndex < 0)
BindThumbList();
}
The following code snippet shows how the "Next >>"
navigation button is created. plLinks is a PlaceHolder
server
control inside the DataList footer. The reason to include the VPath in the link
is that i want the color of links (visited color or not) to be dependent on page
number and also on the virtual directory. Other page navigation links are
created similarly.
plLinks.Controls.Add(new LiteralControl(String.Format(
" <A href=\"javascript:GoToPage('{0}',{1});\">Next >></A>",VPath,CurPage+1)));
Conclusion
I hope that you'll find my thumbnail solution useful and use it. I am not a
professional Web developer and i cannot test it in a Web site, however i use
it to see thumbnails from CDs with images! (my system has Win2000 Pro with PWS
installed). I enjoyed writing the code and actually was my first C# code. The ASP.NET model
simplifies Web programming and makes it similar to desktop programming. Finally,
i would propose to anyone interested to rewrite the ThumbList control as a
custom (rendered) control because according to documentation rendering is faster that
composition. Hidden fields can be emitted programmatically using
the RegisterHiddenField
method of the Page
class. You
could also derive the user control from the
DataList control and override some functions .. i guess :)