|
|||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
A screenshotIntroductionWelcome 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 toolsThe 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
The <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:
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 (
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 classThe 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 // create thumbnail using .net function GetThumbnailImage
bitmapNew = new Bitmap(_path); // load original image
if(!_bStretch) { // retain aspect ratio
widthOrig = bitmapNew.Width;
heightOrig = bitmapNew.Height;
fx = widthOrig/_width;
fy = heightOrig/_height; // subsampling factors
// must fit in thumbnail size
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();
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.
// ---- apply bevel
int widTh,heTh;
widTh = bitmapNew.Width; heTh = bitmapNew.Height;
int BevW = 10, LowA=0, HighA=180, Dark=80, Light=255;
// hilight color, low and high
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);
// ---- draw dark (shadow) sides first
// draw bottom-side of bevel
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);
// draw right-side of bevel
...
// ---- draw bright (hilight) sides next
...
// draw top-side of bevel
...
// draw left-side of bevel
...
// dispose graphics objects and return bitmap
br.Dispose(); newG.Dispose();
return bitmapNew;
Thumbnail HTTP handlerThe 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
_oGenerator.SetParams(_path,_width,_height,_bStretch,_bBevel,_
bUseCOMobject);
Cache MyCache = context.Cache;
sCacheKey = _oGenerator.GetUniqueThumbName();
// --- remove from cache when we want to refresh
bool bRefresh = (context.Request["Refresh"]=="true");
if(bRefresh) MyCache.Remove(sCacheKey);
if(MyCache[sCacheKey] == null)
{ // the thumbnail does not exist in cache, create it...
try {
bitmap = _oGenerator.ExtractThumbnail();
bFoundInCache=false;
}
catch(Exception e) {
// requested image cannot be loaded, try to load the
// 'NoThumb' image
...
}
}
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
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 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 { // separate different filters with ';'
String[] filters = _filter.Split(';');
for(i=0; i<filters.Length; i++) {
files = Directory.GetFiles(_path,filters[i]);
arFilesAll.AddRange(files);
}
}
// create the datasource (list of 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);
} // end if save thumbs
} catch(Exception exDir) { arFilesAll = new ArrayList(); }
// sort file-list if requested
if(Sort)
arFilesAll.Sort();
// page thumblist if requested
ArrayList arFilesToShow;
if(!AllowPaging)
arFilesToShow = arFilesAll;
else { // create a data source with only the page files
int totalPages = (int)Math.Ceiling((float)arFilesAll.Count/PageSize);
if(totalPages==0) totalPages=1;
MyThumbList.Attributes["TotalPages"] = totalPages.ToString();
if(CurPage<1) {
// we have not given a page from caller, get it from hidden field
try {
CurPage = int.Parse(hdnCurPage.Value); //Request.Form["__EVENTARGUMENT"]);
} catch(Exception exParse) { CurPage=1; }
}
if(hdnPrevPath.Value!=VPath) // restart page numbering
CurPage = 1;
Trace.Warn("BindThumbList: Current page=" + CurPage.ToString());
// make sure current page is in the [1,totalPages] range
if(CurPage>totalPages) CurPage=totalPages;
else if(CurPage<1) CurPage=1;
// store curPage in attributes and in hidden field
MyThumbList.Attributes["CurPage"] = CurPage.ToString();
hdnCurPage.Value = CurPage.ToString();
// put files of the page in the dsPageFiles array
int startIndex = (CurPage-1)*PageSize;
int endIndex = Math.Min(CurPage*PageSize,arFilesAll.Count);
arFilesToShow = arFilesAll.GetRange(startIndex, endIndex-startIndex );
} // end define page
// create in-memory DataTable to be the data source
DataTable dtThumbs = CreateCommentsTable(); //see below for this function
DataView dvCommentsFromXML = null;
if(ShowComments || AllowEdit) {
DataSet dsComments = ReadCommentsFromXML();
dvCommentsFromXML = dsComments.Tables[0].DefaultView;
}
// add rows with filenames
for(i=0; i<arFilesToShow.Count; i++) {
// Response.Write(arFilesToShow[i]);
DataRow dr = dtThumbs.NewRow();
dr[0] = arFilesToShow[i];
dtThumbs.Rows.Add(dr);
} // next file
// add existing comments
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]; // copy comment
}
}
MyThumbList.DataSource = dtThumbs.DefaultView;
MyThumbList.DataBind();
} // end method
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
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+"'";
// find row for filename or create a new one if does not exist
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");
// read comments from XML file
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. <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)
{
// register client script to go to a next page
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 not in update, bind every time since parameters may have change
if(MyThumbList.EditItemIndex < 0)
BindThumbList();
}
The following code snippet shows how the "Next >>"
navigation button is created. plLinks is a
plLinks.Controls.Add(new LiteralControl(String.Format(
" <A href=\"javascript:GoToPage('{0}',{1});\">Next >></A>",VPath,CurPage+1)));
ConclusionI 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 | ||||||||||||||||||||||||||||