Image Processing
Tech Note

Slide Show:  Creating a Delphi Image Management Utility

by Earl F. Glynn

First appeared in November 1998 Delphi Developer, pp. 8-11
appearing here with updates and additions.

Contents
Background
Slide Show Program
Printing Matrix of Images
GIF Images
Conclusion
Download

(Chinese Translation by Hector Xiang)


Often when you're designing a user interface for your application or your Web site, you need to look at a large number of images and icons and compare them to see which would work together the best to present the image you want.  The only problem is that it's tough to find an easy way to view a large number of images.  This article shows how to create a utility that will solve this problem in two ways -- a slide show and thumbnail printout.

The slide show displays images one after another, as fast or as slow as you like, forward or in reverse.  The slide show can be shown with images transformed in a number of ways:  RGB composite, Intensity, Red, Green, Blue, Hue, Saturation, Value, Cyan, Magenta, Yellow, Black.  The images can be shown in monochrome or inverted colors.  If you step through the images one-by-one, you can even delete unwanted files.

The thumbnail printout will print a miniature version of your images on paper in a matrix form with a user-specified number of columns.  All of the native Delphi image formats are supported (BMP, JPG, ICO, WMF, and EMF) and other formats, like GIF, can be easily added.

Background

I often deal with a large number of images. Many of these images are part of sequences that require further manipulation. Just finding the "interesting" images from a large number of images can take considerable time. Sometimes I want to step through the images one-by-one and view them on the screen, but often I just want to print the set of images in matrix form as a set of "thumbnail" images.

As a programmer I often want to find an icon for a program or a speed button bitmap. I’ve collected a large number of bitmaps and icons from various Internet sources, but searching through this library can be very time consuming, especially using Windows Explorer. Sometimes when I’m "brainstorming" I just want to step through a large number of button bitmaps or icons, but I’ve always wanted to print these small bitmaps out in matrix form, on a small number of pages.

For example, there were 463 icons in The Icon Book — Visual Symbols for Computer Systems and Documentation. If these icons were printed 25 per row and 15 rows per page, all of them would fit on a page and half. Scanning a single sheet of paper with near 400 icons is much easier and faster than trying to find the "right" icon online. Thousands of icons or bitmaps can be printed on a few sheets of paper, which for me is much easier to scan than using any online program.

The Slide Show Program

The screen for a Delphi 3 SlideShow program is shown in Figure 1. In this example, image 6 of a sequence of 8 BMP images is being displayed in a TImage object. I’ll walk through how to use the program in this section.

Figure 1.  The Slide Show Program

ScreenSlideShow.jpg (53244 bytes)

The file type of the desired images is selected in the TRadioGroup at the upper right.  Since I usually work with a set of a specified type of image, this TRadioGroup is used as a filter in selecting images.

The directory containing the images is selected with the TDriveComboBox and TDirectoryListBox at the right. These "Win 3.1" components still work well. The alternative TDirectoryOutline on the "Samples" component tab is not documented anywhere. (Borland should be really provide online documentation even for their "Samples" instead of forcing programmers to study source code.) The file type and directory list can be chosen in any order or at any time.

Whenever the directory changes, an in-memory TStringList is updated to be a list of all selected graphics files. I only keep a list of filenames in memory, since I sometimes work with so many BMP files there is no way they will fit in a reasonable amount of memory.

Images are always re-fetched from disk whenever needed based on their name. Filling this TStringList is shown in the following UpdateFileList procedure:

// For now, only look for single type of file in directory for inclusion in list
PROCEDURE TFormSlideShow.UpdateFileList;
  CONST
    Sentinel = '|';
  VAR
    FileSpec  : STRING;
    i         : INTEGER;
    Index     : INTEGER;
    Line      : STRING;
    Path      : STRING;
    ReturnCode: Cardinal;
    SearchRec : TSearchRec;
    SortKey   : STRING;
    StepSize  : INTEGER;
    StringList: TStringList;
BEGIN
  Screen.Cursor := crHourGlass;
  TRY
    FileList.Clear;
    Path := DirectoryListBox.Directory;
    FileSpec := '*.' + RadioGroupFileFormat.Items[RadioGroupFileFormat.ItemIndex];

    IF Path[LENGTH(Path)] <> '\'
    THEN Path := Path + '\'; // Just in case

    StringList := TStringList.Create;
    TRY
      // Assume filenames are numbers that should be in order
      StringList.Sorted := TRUE;

      // To find all directories, including ones that may have the "system"
      // attribute, just ask for all files, faAnyFile, here.
      ReturnCode := SysUtils.FindFirst(Path + Filespec, faAnyFile, SearchRec);
      WHILE ReturnCode = 0 DO
      BEGIN
        IF (SearchRec.Attr AND faDirectory <> faDirectory) AND
            (SearchRec.Name <> '.') AND
            (SearchRec.Name <> '..') // exclude '.' and '..' directories
        THEN BEGIN
          Line := Path + SearchRec.Name;
          // Form line:  SortKey + Sentinel + Line
          SortKey := SearchRec.Name;
          SortKey := Format('%20s', [Sortkey]);
          StringList.Add(SortKey + Sentinel + Line)
        END;
        ReturnCode := SysUtils.FindNext(SearchRec)
      END;
      SysUtils.FindClose(SearchRec);

      // Now load sorted StringList into FileList
      FOR i := 0 TO StringList.Count-1 DO
      BEGIN
        Line := StringList.Strings[i];
        Index := POS(Sentinel, Line);
        FileList.Add(COPY(Line,Index+1, LENGTH(Line)-Index))
      END;

    FINALLY
      StringList.Clear;
      StringList.Free
    END;

    LabelFilecount.Caption := InttoStr(FileList.Count) + ' ' +
    RadioGroupFileFormat.Items[RadioGroupFileFormat.ItemIndex] +
        Plural(FileList.Count, ' file', '');
    TrackBarProgress.Position := 0;

    IF FileList.Count > 0
    THEN TrackBarProgress.Max := FileList.Count-1;

    // Heuristic to avoid solid black lines of tick marks when many images
    // present
    StepSize := FileList.Count DIV 50;
    IF StepSize = 0
    THEN StepSize := 1;
    TrackBarProgress.Frequency := StepSize;

    IF FileList.Count = 0
    THEN BEGIN
      ImageDisplay.Picture := NIL;
      LabelImageName.Caption := ''
    END
    ELSE DisplayCurrentImage;

    TrackBarProgress.Visible := (FileList.Count > 1);

    // Make sure Print button available when only single image
    PanelPlay.Visible := (FileList.Count > 0)
  FINALLY
    Screen.Cursor := crDefault
  END
END {UpdateFileList};

Note that the in-memory StringList is used to sort the files -- I probably should have had a "sort" option since sometimes I don't want the list sorted.

Be sure to always use SysUtils.FindClose, …. instead of just FindClose, to avoid a Delphi name-space conflict between two units.

Once a set of images is selected, the TPanel of controls is visible at the lower right. The "stretch" TCheckBox allows an image to be stretched to the size of the TImage (which does not work and is not available for ICO files, however). The VCR-like group of speed buttons (see Figure 2) controls the display of images in the TImage component.

Table 2.  The Speed Buttons
ScreenSlideShowControls.jpg (3098 bytes)

The controls are very simple: the first or last image can be quickly selected using the "end" controls. The sequence can be displayed in forward or reverse direction using the green "play" buttons. The yellow "pause" button can be used at any time to stop the display of a sequence. The Click responses for these speed buttons is shown next:

procedure TFormSlideShow.SpeedButtonFirstOneClick(Sender: TObject);
begin
  TimerStep.Enabled := FALSE;
  TrackBarProgress.Position := 0;
  DisplayCurrentImage
end;

procedure TFormSlideShow.SpeedButtonPlayClick(Sender: TObject);
begin
  // Delta is +1 for forward direction, -1 for reverse direction
  Delta := (Sender AS TSpeedButton).Tag;
  TimerStep.Enabled := TRUE;
end;

procedure TFormSlideShow.SpeedButtonLastOneClick(Sender: TObject);
begin
  TimerStep.Enabled := FALSE;
  TrackBarProgress.Position := FileList.Count-1;
  DisplayCurrentImage
end;

procedure TFormSlideShow.SpeedButtonPauseClick(Sender: TObject);
begin
  TimerStep.Enabled := FALSE;
  DisplayCurrentImage // also enables Print button
end;

The TSpinEdit control (another "Samples" component that Borland should document online) specifies the frequency that images should change in the sequence using a TTimer. Every time the TTimer fires, the image sequence goes forward or backward one image:

procedure TFormSlideShow.TimerStepTimer(Sender: TObject);
  VAR
    Index: INTEGER;
begin
  // Use separate index variable to avoid range error with
  // TrackBarProgress.Position
  Index := TrackBarProgress.Position + Delta;

  IF   (Index < 0) OR (Index > FileList.Count - 1)
  THEN TimerStep.Enabled := FALSE
  ELSE TrackBarProgress.Position := Index;

  DisplayCurrentImage
end;

Note that the progress through the sequence is shown by the TTrackBar at the top at all times, which is shown in the routine that updates the display of an image:

PROCEDURE TFormSlideShow.DisplayCurrentImage;
  VAR
    BitmapRaw : TBitmap;
    BitmapToDisplay: TBitmap;
begin
  IF  (ComboBoxColorSpace.ItemIndex = 0) AND
      (NOT CheckBoxMonochrome.Checked) AND
      (NOT CheckBoxInvert.Checked)
  THEN // Shortcut for "normal" RGB image to reduce overhead
    ImageDisplay.Picture.LoadFromFile( FileList.Strings[TrackBarProgress.Position] )
  ELSE BEGIN
    // This approximates behavior of Picture.LoadFromFile. One difference
    // here is that everything is a TBitmap, while above certain files,
    // e.g., ICOs, cannot be manipulated. So an ICO can be stretched
    // only when it is not shown in full RGB color! I know, this really
    // doesn't make any sense.
    BitmapRaw := LoadGraphicsFile( FileList.Strings[TrackBarProgress.Position] );
    TRY
      BitmapToDisplay := ExtractImagePlane(ColorPlane,
          NOT CheckBoxMonochrome.Checked,
          CheckBoxInvert.Checked,
          BitmapRaw);
      TRY
        ImageDisplay.Picture.Graphic := BitmapToDisplay
      FINALLY
        BitmapToDisplay.Free
      END
    FINALLY
      BitmapRaw.Free
    END
  END;

  LabelImageName.Caption :=
    'Image ' +
    IntToStr(1 + TrackBarProgress.Position) +
    ': ' +
    ExtractFilename( FileList.Strings[TrackBarProgress.Position] )+
    ' (' +
    IntToStr(ImageDisplay.Picture.Width) + '-by-' +
    IntToStr(ImageDisplay.Picture.Height) + ' pixels)';

  // Update state of buttons
  SpeedButtonFirstOne.Enabled := (TrackBarProgress.Position <> 0);
  SpeedButtonPlayReverse.Enabled := (TrackBarProgress.Position <> 0);

  SpeedButtonLastOne.Enabled := (TrackBarProgress.Position <> FileList.Count-1);
  SpeedButtonPlayForward.Enabled := (TrackBarProgress.Position <> FileList.Count-1);

  SpeedButtonPause.Enabled := TimerStep.Enabled;
  BitBtnPrint.Enabled := NOT TimerStep.Enabled
end;

Note the polymorphic behavior of the Picture.LoadFromFile method above. This method can be used to load and display a graphic of any kind that Delphi "understands."   This feature prevents any manipulation of certain files, such as the ICO files.  The ELSE clause bypasses the use of the poloymorphic TPicture and replaces it with a LoadGraphicsFile conversion   (see below for details) to a TBitmap regardless of the type of file.   This allows the color plane manipulation described below.

In addition to loading the image, the label beneath it is updated, as is the state of the various "VCR-like" buttons.

A variety of color plane manipulations can be selected with the controls shown in Figure 3.

Figure 3.  Color Plane Manipulation
ScreenSlideShowColorManipulation.jpg (2371 bytes)

If you pause the Slide Show at any image, press the "Delete" key to delete any image

Printing Matrix of Images

Printing images of the various types described above isn’t completely straightforward. Figure 4 shows that the number of columns on the landscape-printed page can be selected. If you select "1", only a single image will be printed on each page. If you select "20", then 20 images are be printed in a row, with as many rows as possible printed on a page.

Figure 4.  The Slide Show Print Dialog

ScreenSlideShowPrint.jpg (25083 bytes)

The location of the printed image is controlled by using device-independent calculations, since the dot density on printers can vary considerably. The MulDiv function from the System unit is quite useful for take percentages of integer values as shown next:

// 98% divided by number of images per row
iDelta := 980 DIV FormPrint.SpinEditMaxColumns.Value; // 0.1%

// Start 1% from left margin + Delta Width * Column Index
iFromLeftMargin := MulDiv(Printer.PageWidth, (10 + iDelta*Column), 1000);

// 0.5% width between images
iImageWidth := MulDiv(Printer.PageWidth, iDelta-5, 1000);
jImageHeight := MulDiv(iImageWidth, BitmapToPrint.Height, BitmapToPrint.Width);
...
TargetRectangle :=
    Rect(iFromLeftMargin, jFromTopOfPage,
            iFromLeftMargin + iImageWidth, 
            jFromTopOfPage + jImageHeight);

So now that we know where we want to place an image on the Printer canvas, how can we print it? The StretchDIBits API call must be used so images are printed correctly on a variety of Windows printers. (You’re very lucky if you’re using StretchDraw and haven’t had problems printing images.) The details of this are in the code you can download. I’ll briefly discuss the highlights here.

The next problem is that JPG, ICO, etc. files cannot be printed without first converting them to a TBitmap. This conversion is performed by re-reading each image just before printing, and converting each graphic type to a TBitmap using the Draw method shown next in the LoadGraphicFile Function:    [Note:  a newer and better version of LoadGraphicsFile can be found in the Magnifier Lab Report.]

// Create TBitmap from BMP, JPG, WMF, EMF or GIF disk file.
// Could be easily extended to other image types.
FUNCTION LoadGraphicsFile(CONST Filename: STRING): TBitmap;
  VAR
    Extension: STRING;
{$IFDEF GIF}
    GIFImage : TGIFImage;
{$ENDIF}
    Icon : TIcon;
    JPEGImage: TJPEGImage;
    Metafile : TMetafile;
BEGIN
  RESULT := NIL; // In case anything goes wrong

  IF   FileExists(Filename)
  THEN BEGIN
    Extension := UpperCase( COPY(Filename, LENGTH(Filename)-2, 3) );

    // Quick and dirty check that file type is OK
    ASSERT( (Extension = 'BMP') OR
        (Extension = 'EMF') OR
{$IFDEF GIF}
        (Extension = 'GIF') OR
{$ENDIF}
        (Extension = 'ICO') OR
        (Extension = 'JPG') OR
        (Extension = 'WMF') );

    RESULT := TBitmap.Create;

    // BMP File -- no additional work to get TBitmap
    IF Extension = 'BMP'
    THEN RESULT.LoadFromFile(Filename);

{$IFDEF GIF}
    // GIF File
    IF   Extension = 'GIF'
    THEN BEGIN
      GIFImage := TGIFImage.Create;
      TRY
        GIFImage.LoadFromFile(Filename);
        RESULT.Height := GIFImage.Height;
        RESULT.Width := GIFImage.Width;
        RESULT.PixelFormat := pf24bit;
        RESULT.Canvas.Draw(0,0, GIFImage)
      FINALLY
        GIFImage.Free
      END
    END;
{$ENDIF}

    // ICO File
    IF   Extension = 'ICO'
    THEN BEGIN
      Icon := TIcon.Create;
      TRY
        TRY
          Icon.LoadFromFile(Filename);
          RESULT.Height := Icon.Height;
          RESULT.Width := Icon.Width;
          RESULT.PixelFormat := pf24bit; // avoid palette problems
          RESULT.Canvas.Draw(0,0, Icon)
        EXCEPT
          // Ignore problem icons, e.g., Stream read errors
        END;

      FINALLY
        Icon.Free
      END
    END;

    // JPG File
    IF   Extension = 'JPG'
    THEN BEGIN
      JPEGImage := TJPEGImage.Create;
      TRY
        JPEGImage.LoadFromFile(Filename);
        RESULT.Height := JPEGImage.Height;
        RESULT.Width := JPEGImage.Width;
        RESULT.PixelFormat := pf24bit;
        RESULT.Canvas.Draw(0,0, JPEGImage)
      FINALLY
        JPEGImage.Free
      END
    END;

    // Windows Metafiles, WMF or EMF
    IF  (Extension = 'WMF') OR
        (Extension = 'EMF')
    THEN BEGIN
      Metafile := TMetafile.Create;
      TRY
        Metafile.LoadFromFile(Filename);
        RESULT.Height := Metafile.Height;
        RESULT.Width := Metafile.Width;
        RESULT.PixelFormat := pf24bit; // avoid palette problems
        RESULT.Canvas.Draw(0,0, Metafile)
      FINALLY
        Metafile.Free
      END
    END;

  END;
END {LoadGraphicFile};

Note that the bitmap created by LoadGraphicFile must be freed by the calling routine to avoid a memory leak. So now with the TargetRectangle from above, and the TBitmap returned by LoadGraphicFile, the PrintBitmap routine can be called:

// Based on posting to borland.public.delphi.winapi by Rodney E Geraghty,
// 8/8/97. Used to print bitmap on any Windows printer.
PROCEDURE PrintBitmap(Canvas: TCanvas; DestRect: TRect; Bitmap: TBitmap);
  VAR
    BitmapHeader: pBitmapInfo;
    BitmapImage : POINTER;
    HeaderSize  : DWORD;
    ImageSize   : DWORD;
BEGIN
  GetDIBSizes(Bitmap.Handle, HeaderSize, ImageSize);
  GetMem(BitmapHeader, HeaderSize);
  GetMem(BitmapImage, ImageSize);
  TRY
    GetDIB(Bitmap.Handle, Bitmap.Palette, BitmapHeader^, BitmapImage^);
    StretchDIBits(Canvas.Handle,
        DestRect.Left, DestRect.Top, // Destination Origin
        DestRect.Right - DestRect.Left, // Destination Width
        DestRect.Bottom - DestRect.Top, // Destination Height
        0, 0, // Source Origin
        Bitmap.Width, Bitmap.Height, // Source Width & Height
        BitmapImage,
        TBitmapInfo(BitmapHeader^),
        DIB_RGB_COLORS,
        SRCCOPY)
  FINALLY
    FreeMem(BitmapHeader);
    FreeMem(BitmapImage)
  END
END {PrintBitmap};

Here's an example (a scanned printout) of a large number of faces in a "Thumbnail Printout" (look for "Face Database" here for information about this set of images ):

Figure 5.  Sample Thumbnail Printout

ORLFaces.jpg (24320 bytes)

GIF Images

Support for GIF images is not provided in Delphi, probably due to legal issues related to a Unisys patent and royalties it requires. GIF support is not provided directly with the downloaded source code due to this, but instructions are provide for downloading a component that provides GIF support. If you have GIF support, you can enable its use with this program by defining a "GIF" conditional during compilation.   Download Anders Melander's TGIFImage from www.melander.dk/delphi/gifimage.   Find additional information about Delphi Graphics File Formats & Conversions here.

To change the compilation conditional:  Project | Options | Directories/Conditionals
On the "Conditionals" line, use GIF for GIF support or NoGIF if Anders Melander's TGIFImage is not installed.


Conclusion
There are a number of enhancements that could be added to Slide Show. I hope you find Slide Show a useful starting point for building a customized program for your needs.

Download
D3-D5 Source Code and EXE:  SlideShow.ZIP (240 KB)

Modified 5 August 1999 to correct a problem when the last working directory, which was fetched from the INI file during FormCreate, no longer exists.  I encountered this problem when mapped network drives were later not mapped.

Modified 17 Feb 2000 to make "NoGIF" the default compiler conditional and "fix" warnings received in D5.

Similar Utilities:

View Shot
http://members.xoom.com/_XOOM/jescott/ConvertViewShot.html  

Slide Show:  Program to make a slide show from a list of JPEG images.
http://www.david-taylor.pwp.blueyonder.co.uk/software/imaging.html 


Carsten Kronenberg reports that Windows 95 has serious resource leaks when processing hundreds of WMF files.  The same problem has not been observed in Windows 98 or Windows 2000.


Updated 18 Feb 2002


since 15 Feb 1999