*** NOTE ***
A vastly superior variant of this application is available here: /Articles/94893/Add-NET-thumbnailing-to-a-classic-ASP-Multi-upload. It requires no DLL registration because it utilizes ASP.NET to render thumbnails. I highly suggest using that version of the gallery. Read both articles to get all the details of how the gallery works but use the ASP.NET version for best results. Thanks! -- LB
Introduction
This is an ASP based image gallery with multi-upload and auto-thumbnail capability. Galleries are displayed using SimpleViewer - a Flash based gallery application. The application utilizes ADO/XML and text files to manage gallery information, so there is no need for database configuration.
Background
This program utilizes the following Open Source applications:
- SimpleViewer - A Flash based image gallery with flexible configuration options.
- FreeASPUpload - A VBScript based ASP class for handling file uploads.
- CXImage Control - A DLL file for image manipulation using COM languages such as VBScript. This DLL must be registered on your IIS server. I'm working on a version that uses .NET for thumbnailing. Look for it in a follow-up article.
- Prototype.js - AJAX Framework.
- Scriptaculous - Effects framework based on Prototype.js.
Using the Code
To install the code, drag the gallery folder found in gallery.zip to your IIS folder. Set the images subfolder to enable write access, or you will receive permission denied errors. Browse to your gallery folder and start using the application.
The login and password are "admin" and "admin", respectively. You can change these in the handle_login
function found in gallery_app.asp.
Upon login, you'll see a list of existing galleries.
Points of Interest
There is nothing truly outlandish about this code, but there are some interesting areas:
- Multi-file upload - allows for up to nine files to be uploaded in batch
- ADO/XML - The use of ADO recordsets and XML to store them as files, versus using a database.
- An un-utilized feature - A VB.NET based thumbnail generator that if you do some re-coding can get this tool working on hosted sites like godaddy.
- Drag and drop sorting - The code leverages Scriptaculous' sort feature to enable drag and drop arrangement of the image list.
Multi-File Upload
By combining a hidden IFrame with AJAX requests via prototype.js, the upload page (upload_files.asp) allows up to nine images to be uploaded in batch. As each file is uploaded, an indicator displays adjacent to the current upload. When a file upload is complete, a thumbnail displays adjacent to the form and the next file is processed. When the batch is complete, a button displays prompting to click and upload more files.
ADO/XML
Each gallery utilizes an ADO/XML recordset to manage the file list and follow sort order. Using this technology lets us take advantage of structured storage without having a database setup. Unique IDs are generated for each image using the newid()
function found in gallery_app.asp.
An Un-Utilized Feature
There is a file thumbnail.aspx and a corresponding thumbnail.aspx.vb code-behind page that can be utilized to generate thumbnails. The upside of using this page is it does away with the CXImage DLL, which is beneficial in hosting environments that will not allow for controls to be registered. The downside is you'll have to do some re-coding in order to leverage the .NET based thumbnailer. I hope to address this change in a future article.
Drag and Drop Sorting
The "Arrange" link leads to a page that lets you drag and drop gallery images to re-arrange their position in the gallery view. This feature utilizes Scriptaculous "sortables" to accomplish the task.
The Code Explained
The bulk of this application's code can be found in gallery_app.asp. Each page is a stub that holds the HTML content, and makes calls to functions within gallery_app.asp. Several pages also utilize AJAX calls to "functional pages" to perform operations behind the scenes and provide a more engaging user experience. Let's run through each page, I'll explain the functionality, then dive into how the code works and where the AJAX calls are made. I'll also explain what pages are called. First, a little background.
Libraries and Naming Conventions
I use prototype.js for my AJAX framework and scriptaculous.js for effects. I use a "code behind" convention for JavaScript to separate JavaScript code from the HTML. You'll find the JavaScript code for a given page named as "pagename.js". For example, if the page is named upload_files.asp, you'll find its JavaScript library named upload_files.js.
The main code library for the server-side code is gallery_app.asp.
AJAX Calling Convention
You'll see the same basic structure appear on pages where I use AJAX calls. Using Prototype's Ajax.Request
method, a basic template for the method looks like this:
new Ajax.Request('action_page.asp?gallery_id=' + id, {
method: 'post',
postBody: $('form_name').serialize(),
onSuccess: function(transport){
$('results_container').innerHTML=transport.responseText;
},
onFailure: function(transport) {
var err = "<br/>Error creating gallery"+transport.responseText;
$('results_container').innerHTML=$('results_container').innerHTML + err;
}
});
'action_page.asp' is the name of the page that performs a designated action. In most cases, the action form re-renders the part of the page being acted upon to reflect the results. form_name
is the ID of the form that contains data to be posted to action_page.asp. 'results_container
' is the ID of a div tag that is used to display the results of the request; in most cases, this is the container that holds the calling form. On successful calls, the contents of this container are replaced with the rendered content passed back in the AJAX response.
Here's a sample of the toggle gallery function used to make galleries show/hide:
function toggleGallery(id) {
new Ajax.Request('toggle_gallery.asp?gallery_id=' + id, {
method: 'post',
onSuccess: function(transport){
$("gallery_list").innerHTML=transport.responseText;
},
onFailure: function(transport) {
$("gallery_list").innerHTML=$("gallery_list").innerHTML +
"<br/>Error creating gallery"+transport.responseText;
}
});
}
In the above example, I'm simply passing the gallery ID; some calls also include data from a form. If I am posting data from a form, you'll also see the following code line below the method 'post
'.
postBody: $('form_name').serialize(),
Where 'form_name
' is the ID of the HTML form that is being passed to the page.
OK, now let's hit the content pages.
gallery.asp
This is the main display page for the gallery. It calls the Flash "SimpleViewer.swf" file. It also passes the gallery ID to the page gallery.xml.asp which assembles an XML document that SimpleViewer uses to construct the list of images. Along the top, you'll see a menu of your gallery titles and an admin link. Clicking the admin link brings you to the gallery list page.
gallery_list.asp
This page shows the list of your existing galleries; from here you can: toggle the show hide status of a gallery, create a new gallery, access Info, Upload, Images (captions), and view the gallery via gallery.asp.
"Create a new gallery" makes an AJAX call to the action page create_gallery.asp which does four things: get a new ID [id
] by incrementing the gallery ID counter found in images/id.txt, create a new gallery info file [id].txt, a new gallery table to hold the image list and sort order [id].xml, and finally a gallery images and thumbnail folder [id] and [id]/thumb. [id] is replaced with the new ID. See the function "CreateGallery
" in gallery_app.asp for more details.
The "Hide/Show" buttons in the rightmost columns make AJAX calls to toggle_gallery.asp which adds/removes the gallery ID from the "hidden" list found in images/config.txt. The gallery ID is passed in the querystring.
In the Tools column, you'll find four links. Info lets you edit/update the gallery title. Upload lets you upload new images to the gallery. Images lets you mange images (add/remove) as well as edit captions. Finally, View lets you see your changes in SimpleViewer.
edit_gallery.asp
Edit gallery info lets you change the gallery title and description. The description is not used anywhere at this point. This is a pretty no frills form. When you click Save, an AJAX call is made to the action page save_gallery.asp which makes a call to the function SaveGallery()
found in gallery_app.asp. The information on this page is saved to a text file "images/gallery_id.txt", where gallery_id
is the ID number of the passed in querystring (parameter: gallery_id
). The ID is passed automatically when you click the "info" link from the gallery_list.asp page.
upload_files.asp
This is where this whole project started. I wanted to find a way to upload multiple files using ASP, but at the same time provide some feedback as to the uploading process. Ideally, this would come in the form of a progress bar for each file; however, I couldn't figure out a way to do that. So I did the next best thing: add an indicator image (a spinner if you like) indicating the current upload.
To upload files, click the "Browse..." button and locate the desired image from your hard drive (depending on your browser, the button might say "Choose..."). You can upload up to nine images in one batch. When you have selected all the images you wish to upload, click the "Start uploading images" button.
Upload Progress
As each file uploads, the spinner indicates the current upload (look at the third image). When an upload completes, a thumbnail is placed adjacent the browse (or Choose) button and a larger thumbnail along with the image size is placed under the title "Last Completed Upload". The process continues for each image until the list is complete.
Upload Complete
When the last image is uploaded, a button appears that prompts you to upload more images if you wish. The process can be repeated as many times as you like.
So what happens when the "Start uploading images" button is clicked? A JavaScript function "uploadClick();
" is invoked (found in upload_files.js). This function iterates through each form, setting it to disabled, and also setting the thumbnail image to blank. Disabling the forms prevents the user from interacting with the form during the upload process.
var upload_counter = 0
function uploadClick() {
$("startupload").fade();
upload_counter = 1;
for (x=1;x<10;x++) {
$("uploaded_image0"+x).src="blank.gif";
$("file_upload_form0" + x).disable();
}
handleForm();
}
This works because each image is stored in its own HTML form. Each form contains a file upload input box, a place holder for the upload thumbnail, and a placeholder for the progress indicator (spinner). Each form and all of its elements are named with a 00, 01, 02 etc... extension to make accessing them using an iterator easy. Also, notice the "target
" attribute of the form is set to 'upload_target
'; more on that later. Lastly, notice that the "action
" attribute is upload.asp; this is the action page that processes uploads.
<form id='file_upload_form02' target='upload_target' method='post'
enctype='multipart/form-data' action='upload.asp?gallery_id=1'>
Image:
<input name='file' id='file02' size='40' type='file' />
<img id='uploaded_image02' width='24' height='24' src='blank.gif'/>
<img src='indicator.gif' id='upload_indicator02' style='display: none;' />
<br />
</form>
At the end of uploadClick
, the function handleForm()
is called (also located in upload_files.js). Handleform
increments the counter for a file, turns on the appropriate indicator, and submits the form by enabling it, submitting, then disabling it again.
function handleForm() {
while ( ($("file0" + upload_counter )!=null)
&& ($("file0" + upload_counter ).value != "" )
&& (upload_counter<10) )
upload_counter++;
if ( upload_counter<=9 ) {
$("upload_indicator0" + upload_counter).appear();
$("file_upload_form0" + upload_counter).enable();
$("file_upload_form0" + upload_counter).submit();
$("file_upload_form0" + upload_counter).disable();
} else {
batchComplete();
}
}
Each form submits to a hidden IFrame (the same IFrame) located below the list of forms. This IFrame acts as the target for the upload form. Here is the IFrame source:
<iframe
id='upload_target'
name='upload_target'
src='about:blank'
style='width:0;height:0;border:0px solid #fff;' >
</iframe>
Since the target of each form is set to 'upload_target
' and our IFrame is named 'upload_target
', the submit request is sent to the IFrame rather than the whole page. This allows each file to be uploaded in sequence while not disturbing the main HTML page (upload_files.asp). The IFrame also serves as the return mechanism when the file is complete.
When every file has been uploaded, batchComplete
is called, which simply turns on the button, prompting to execute another batch.
upload.asp
Before I continue with the content pages, I think it is a good point to go into the details on the uploads action page. I won't detail all the action pages as it is easy enough to describe their functionality in a sentence or two. Upload.asp is a different beast, so I'll single it out here.
Upload.asp is an action page that incorporates FreeASPUpload
to enable file uploading. The neat thing is that it is free and Open Source, so you can tweak and alter its behavior to your heart's desire. It is also free, so you can use it without registering DLLs. This page uses another Open Source project (CXImageControl
) to generate thumbnails; unfortunately, this control requires server side registration. You can easily adapt the thumbnail function to work with whatever image control they provide on the server. I'm also working on a follow up article (provided this one provokes enough interest) that will utilize a single .NET page to do thumbnails thus making this gallery "component free" and easy to run on hosting services that are not "DLL registration friendly".
Here is a rundown of how uploads.asp works ([gallery_id] in the bullets below is a placeholder for whatever the real gallery ID is as passed in the querystring).
- Create and call
FreeASPUploads
to load the image file. - Call the
FreeASPUploads
object to save the files to images/[gallery_id]/. - Create a timestamp prefix (four digit year + '.' + day of year + '.' + timer).
- Rename each file with the timestamp to prevent naming conflicts.
- Generate thumbnail images in the following formats: 32x32, 64x64, 96x96, 120x120, 240x240, 480x480, 640x640, 800x800, and 960x960. Thumbnails are placed in images/[gallery_id]/thumb/ and are named filename.WxH.jpg.
- The file is registered with the gallery by adding it to the images/[gallery_id].xml file; this is done using ADO. I'll explain that shortly.
The ProcessUpload
function is a modified version of ProcessUpload
that is included with the upload_test.asp file in the FreeASPUpload
package. The code snippet is rather long so I won't include it here. I will go over the parts I changed and get into the functions called by ProcessUpload
.
The Date Stamp
The date stamp is used to give each file a unique name to avoid file name conflicts.
sStamp = year(now()) & "." & datepart( "y", now() ) & "." & CLng( Timer ) & "."
The uploaded file starts off with the same name as it had on your hard drive so we have to rename it using the MoveFile
method of Scripting.FilesystemObject
. This is shown in the code below as "fso.
". This code snippet can be found starting at line 63 of upload.asp.
strFile = Upload.UploadedFiles(fileKey).FileName
sOldFile = uploadsDirVar & "\" & strFile
strFile = sStamp & strFile
strFileName = uploadsDirVar & "\" & strFile
call fso.MoveFile( sOldFile, strFileName )
After the file is renamed, it is registered in the gallery using the following code line (line 74 of upload.asp):
AddGalleryImage( strFile )
AddGalleryImage
loads the current gallery file (images/gallery_id.xml) by making a call to OpenTable(filepath, rs)
with the full path to the file and a recordset object, appending the new file to the recordset, and saving the recordset as XML using a call to SaveTable(filepath, rs)
which writes the updated data back to the XML file. Here are three functions along with a fourth that creates a new empty gallery table; these functions are found in gallery_app.asp:
function AddGalleryImage(sFileName)
dim path
dim rs
dim lOrder
path = server.mappath(".") & "\images\" & lGalleryID & ".xml"
OpenTable path, rs
rs.addnew
rs("id")=newid()
rs("dateadded")=now()
rs("filename")=sFileName
rs("order")= year(now)*10000+month(now)*100+day(now)
rs.update
SaveTable path, rs
end function
function SaveTable(FullPath, rs)
DeleteFileIfExists FullPath
if not rs.bof then
rs.movefirst
end if
rs.Save FullPath, 1
end function
function OpenTable(FullPath, rs)
set rs=CreateObject("ADODB.Recordset")
rs.Open FullPath, "Provider=MSPersist", , , &H0200
end function
function CreateGalleryTable(FullPath)
dim rs
set rs = CreateObject("ADODB.Recordset")
rs.CursorLocation = 3
rs.Fields.Append "ID", 200, 40
rs.Fields.Append "FileName", 200, 240
rs.Fields.Append "Caption", 200, 4096
rs.Fields.Append "DateAdded", 7
rs.Fields.Append "Size", 3
rs.Fields.Append "Order", 3
rs.Open
SaveTable FullPath, rs
set rs = nothing
end function
Finally, a series of calls is made to the Thumbnail
function to generate each thumbnail image. Thumbnail
takes these parameters: Filename
, Width
, Height
, KeepAspectRatio
, and JpegCompressionRatio
. If KeepAspectRatio
is 0, the image will be stretched or shrunk to fit the exact dimensions; if it is 1, the image will be scaled to fit the width dimension passed.
Call ThumbNail(strFileName, 32, 32,1,80)
Here is the full ThumbNail
function. You'll see where I used CXImageControl
to open the original file, resize it, and save it to the thumb folder.
function ThumbNail(strFileName,lWidth,lHeight,lKeepRatio,lCompression)
' response.Write "Call to Thumbnail"
dim fso: set fso = createobject("scripting.filesystemobject")
dim lH: lH=lHeight
dim lW: lW=lWidth
dim lRatio: lRatio = lKeepRatio
dim strSitePath
''response.write strFileName
strSitePath = fso.GetFile(strFileName).ParentFolder
dim strHTML
dim strSRC
dim strOnC ' OnClick
dim strImgPath
dim strThumbPath
dim strURL
dim bExists
dim strThumbName
strImgPath = strFileName
rem convert to cx image
dim pWidth
dim pHeight
dim FileName
dim sRoot
dim sFile
dim bStretch
dim widthTh
dim heightTh
dim widthOrig
dim heightOrig
dim objCxImage
dim Quality
dim strResult
rem use cximage
bStretch = (lKeepRatio = 0)
Quality = lCompression
sRoot = strSitePath
sFile = fso.getFile( strFileName ).name
FileName = strFileName
pWidth = lW
pHeight = lH
ThumbName = uploadsDirVar & "thumb\" & sFile & "." & _
pWidth & "x" & pHeight & ".jpg"
' Create COM CxImage wrapper object
Set objCxImage = CreateObject("CxImageATL.CxImage")
Call objCxImage.Load(FileName,GetFileType(FileName))
Call objCxImage.IncreaseBpp(24)
' determine thumbnail size and resample original image data
If bStretch Then ' stretch to fit
widthTh = Width
heightTh = Height
Else ' retain aspect ratio
widthOrig = CDbl(objCxImage.GetWidth())
heightOrig = CDbl(objCxImage.GetHeight())
fx = widthOrig/pWidth
fy = heightOrig/pHeight 'subsample factors
' must fit in thumbnail size
If fx>fy Then f=fx Else f=fy ' Max(fx,fy)
If f<1 Then f=1
widthTh = Int(widthOrig/f)
heightTh = Int(heightOrig/f)
End If
objCxImage.SetJpegQuality( Quality )
Call objCxImage.Resample(widthTh,heightTh,2)
call objCxImage.Save(thumbname,GetFileType("jpg"))
Call objCxImage.Destroy()
set objCxImage = nothing
rem end convert to cx image
Thumbnail = ThumbName
end function
OK, now that we see how upload.asp works, let's finish off with a quick roundup of the remaining pages.
captions.asp
Captions.asp is where you manage image captions (the text that appears with the image) and remove images from the gallery; this page is accessed anywhere you see an "images" link. The onChange
event of each caption box triggers the saveCaptions
function found in captions.js. This function calls the action page savecaption.asp which takes the submitted data and saves it to images/[gallery_id]/[imagename].txt.
function saveCaption( obj ) {
var data = obj.innerText
var tag = obj.id
var file = $("file." + tag).value
if (data =="") data = " ";
$("indicator."+tag).show();
new Ajax.Request( "savecaption.asp?gallery_id=" + gallery_id + "&file=" + file, {
method: 'post',
postBody: data,
onSuccess: function(transport) {
$("indicator."+tag).fade();
}
});
}
The textarea code that triggers this call looks like this:
<textarea
onchange='saveCaption(this);'
style='margin: 6px;' rows=4 cols=40
name='caption'
id='IMG_3258.JPG'></textarea>
Notice the onChange
event is set to 'saveCaption(this)
' so all you have to do is change a caption and it is automatically saved. No need to click Submit.
When the Remove button is clicked, the function removeImage
is invoked, which removes the image by calling the action page removeimage.asp. It works similar to the call to saveCaption
, but fades the form onSuccess
. I'll leave you to study that function on the site.
sort.asp
Sort.asp is accessed anywhere you see an "arrange" link. It brings up a list of the files in a grid as they will be displayed in the SimpleViewer Flash. You simply drag and drop the images where you want them and their order is maintained. The sort list is generated by the function ShowSortList
found in gallery_app.asp.
function ShowSortList
dim fso
dim spath
dim fil
dim sExt
dim sFile
dim rs
set fso = createobject("scripting.filesystemobject")
sPath = server.mappath(".") & "\images\" & lGalleryID & "\"
sFile = server.mappath(".") & "\images\" & lGalleryID & ".xml"
OpenTable sFile, rs
rs.sort = "order asc"
do until rs.eof
response.write "<li id='item_" & rs(0).value & "'>"
response.write "<img src='images/" & lGalleryID & "/thumb/" & _
rs(1).value & ".96x96.jpg'/> "
response.write "</li>"
rs.movenext
loop
end function
Near the bottom of sort.asp, you'll see the following JavaScript which creates the sortable page and sets the target to the sort.events.asp action page.
<script language="javascript">
Sortable.create("sort_list",
{
onUpdate: function()
{
new Ajax.Request("sort.events.asp?gallery_id=<%=lGalleryID%>",
{
method: "post",
parameters: { data: Sortable.serialize("sort_list") }
}
);
}
}
);
</script>
sort.events.asp receives as data the list of images in their new order. It reads the list of sorted items in order and sets their "order" value in the gallery to their new ordinal value. This uses the OpenTable
and SaveTable
methods. See the code below to see how I did the sort.
handle_sort_list
function handle_sort_list
?>
dim sData
dim aryData
dim sSQL
dim iX
dim list
dim sPath
dim sFile
dim rs
dim str
' lGalleryID is a global variable set using request.querystring("gallery_id").
sPath = server.mappath(".") & "\images\" & lGalleryID & "\"
sFile = server.mappath(".") & "\images\" & lGalleryID & ".xml"
sData = URLDecode( request.form("data") )
if instr( sData, "&" ) = 0 then exit function
sData = replace( sData, "sort_list[]=", "", 1, -1, 1 )
aryData = split( sData, "&" )
str = str & "GalleryID: " & lGalleryID & vbcrlf
str = str & vbcrlf & vbcrlf
str = str & sData & vbcrlf & vbcrlf
set list = createobject("scripting.dictionary")
str = str & "upper bound: " & ubound( aryData ) & vbcrlf & vbcrlf
' Read each file in order into a dictionary object
' where filename is key and order is value
for iX = 0 to ubound( aryData )
list.add aryData( iX ), iX
str = str & aryData( iX ) & vbtab & ix & vbcrlf
next
str = str & vbcrlf & vbcrlf
' Now open the gallery ado recordset
OpenTable sFile, rs
' iterate the file list
do until rs.eof
str = str & rs("id") & vbtab & list( rs("id").value ) & vbcrlf
' set the order of the file by getting the value from the dictionary object
rs("order")=list( rs("id").value )
rs.update ' save changes
rs.movenext
loop
str = str & vbcrlf
SaveTable sFile, rs ' Call Save Table to update the gallery list
end function
I believe that covers the main functionality of the application. Let me know if you have any questions. Thanks for reading, and enjoy tinkering with the image gallery.
Update: I posted part II of this article here: Add .NET thumbnailing to a classic ASP multi-upload image gallery.