The first article in the new dotImage Customer Articles and Source forum will be submitted by us. Due to the overwhelming request for an efficient thumbnail viewer control, we have opted to create one that isn’t quite “production” quality, but good enough for general comsumption as long as the source is provided. Here is a screenshot:
This thumbnail viewer control has the following features:
- Load an image from a file or AtalaImage directly and automatically resizes the image to a thumbnail and displays it
- Asynchronous Processing
- Scaled JPEG loading for ultra fast thumbnails of JPEG images
- Uses Scale-To-Gray to quickly resize 1-bit images into grayscale thumbs
- Extends the ListView control
When an image is loaded into the control, it will keep the thumbnail in memory unless it’s removed. Therefore, it’s probably not the best control to use for a folder of 10,000 images. That is something that is left to the developer, or for a future production quality control. However for general use, I think this control is adequate. A 100x100 thumbnail only takes up 29 Kb, so it would take 1000 thumbnails to use up 290 Mb, (about equivalent to viewing a 5 Mega-Pixel image).
This ThumbView control was created by inheriting from the ListView control, part of the .NET Framework. Therefore all features of the ListView are part of ThumbView. By default, it will display the thumbnails in LargeIcon view, with each thumbnail being represented by an image index in an associated ImageList (also part of the .NET Framework). When a thumbnail is added to the control either via AddFromFile or AddFromImage, it creates an item in the ListView with a blank image. The next step is tol run a custom ImageCommad called MakeThumbCommand which loads the image and create the thumbnail. Because this command will be run in another thread, we use the ProcessFinished event of the Workspace object event to let the ThumbView control know that the thumbnail is ready.
By taking advantage of the extensibility of dotImage and the .NET Framework, creating an efficient multi-threaded thumbnail viewer is a fairly simple process with only intermediate programming knowledge required.
There are some improvements we could make to this control. The major one would be to make this ListView owner-drawn and virtual. This would take some fairly complex Win32 code, and if created would be implemented as part of the dotImage WinControls in the future. This would give the control much more flexibility including the ability to only load the images that are being viewed (or about to be viewed). And allows for a directory of unlimited number of images to be displayed. This would also give some more flexibility of the thumbnail spacing, and possible effects like drop shadows and watermarks.
If anyone would like to improve the code, please feel free to post your results here. See the attached zip file for the entire C# source code. It would be great if someone here would convert the code to VB for other VB users.
The most interesting code from the ImageProcessing / dotImage perspective in the MakeThumbCommad class with scaled JPEG and resampling example. Here is that portion of the code. See the attached file for the complete project.
MakeThumbCommand.cs
public class MakeThumbCommand : ImageCommand
{
private Size size;
private Color backgroundColor;
private ImageList imageList;
private ListView listView;
private string fileName;
private bool inPlace = false;
public MakeThumbCommand(Size size, Color backgroundColor, ListView listView, string fileName, ImageList imageList)
{
this.size = size;
this.backgroundColor = backgroundColor;
this.imageList = imageList;
this.listView = listView;
this.fileName = fileName;
}
//TODO: Include public properties here so they can be accessed once the command is created
public override AtalaImage ApplyToImage(AtalaImage image)
{
AtalaImage srcImage;
double zoom = 0;
//load the image from a file if a filename is not empty
if (fileName.Length > 0)
{
//----------------------------------------------------
//Load image from a file
//----------------------------------------------------
//take advantage of a JPEG and open it scaled (improves performance)
ImageInfo info = RegisteredDecoders.GetImageInfo(this.fileName);
if (info.ImageType == ImageType.Jpeg)
{
JpegDecoder jpg = new JpegDecoder();
//find out the zoom factor required for the thumbnail
zoom = GetZoomFromSize(this.size, info.Size);
//set the JPEG Scale factor
if (zoom <= 0.125)
jpg.ScaleFactor = JpegScaleFactor.Eighth;
else if (zoom <= 0.250)
jpg.ScaleFactor = JpegScaleFactor.Quarter;
else if (zoom <= 0.500)
jpg.ScaleFactor = JpegScaleFactor.Half;
else
jpg.ScaleFactor = JpegScaleFactor.Full;
//open stream for filename and open it with the scaled JPEG Decoder
Stream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
srcImage = new AtalaImage(fileStream, 0, jpg, null);
fileStream.Close();
}
else
//image is not a JPEG, open it normally
srcImage = new AtalaImage(this.fileName, null);
}
else
srcImage = image;
//-----------------------------
//Make thumbnail from image
//-----------------------------
zoom = GetZoomFromSize(this.size, srcImage.Size);
AtalaImage newImage;
if (zoom < 1)
{
AtalaImage tmpImage = srcImage;
ImageCommand cmd = null;
Size newSize = new Size((int)(srcImage.Width * zoom), (int)(srcImage.Height * zoom));
switch (srcImage.PixelFormat)
{
case PixelFormat.Pixel1bppIndexed:
//scale to gray
cmd = new Atalasoft.Imaging.ImageProcessing.Document.ResampleDocumentCommand(
new Rectangle(Point.Empty, srcImage.Size),
newSize,
ResampleMethod.Default);
break;
case PixelFormat.Pixel8bppIndexed:
//scale to RGB
cmd = new ResampleColormappedToRgbCommand(newSize);
break;
default:
//resize normally
cmd = new ResampleCommand(newSize);
if (srcImage.PixelFormat != PixelFormat.Pixel24bppBgr)
//make the image 24-bit so it's compatible with the image list
tmpImage = srcImage.GetChangedPixelFormat(PixelFormat.Pixel24bppBgr);
break;
}
newImage = cmd.ApplyToImage(tmpImage);
if (tmpImage != srcImage)
tmpImage.Dispose();
}
else
{
//use the current image
if (srcImage.PixelFormat != PixelFormat.Pixel24bppBgr)
newImage = srcImage.GetChangedPixelFormat(PixelFormat.Pixel24bppBgr);
else
newImage = srcImage;
}
//if image doesn't fit size, then we'll resize canvas
AtalaImage finalImage;
if (!(srcImage.Width == this.size.Width && srcImage.Height == this.size.Height))
{
ResizeCanvasCommand resizeCanvas = new ResizeCanvasCommand(this.size, new Point((int)((this.size.Width - newImage.Width) / 2), (int)(this.size.Height - newImage.Height) / 2), this.backgroundColor);
finalImage = resizeCanvas.ApplyToImage(newImage);
}
else
finalImage = newImage;
if (newImage != srcImage)
newImage.Dispose();
//if the image didn't change
//set the in place processing property to true so the workspace doesn't dispose it
if (image == finalImage)
inPlace = true;
return finalImage;
}
//Workspace uses this to determine if the image should be autodisposed or not
public override bool InPlaceProcessing
{
get { return inPlace; }
}
//this function calculates the zoom level of the current image in order to fit to the thumbnail dimensions
private double GetZoomFromSize(Size thumbSize, Size imageSize)
{
if ((double)imageSize.Width / thumbSize.Width > (double)imageSize.Height / thumbSize.Height)
//size to the width
return (double)(thumbSize.Width) / imageSize.Width;
else
//size to the height
return (double)(thumbSize.Height) / imageSize.Height;
}
}