How to generate thumbnails which preserve the aspect ratio

Reverend Jim 3 Tallied Votes 2K Views Share

As part of a project to manage my collection of photos, I wanted to add the capability of displaying a series of files as thumbnails. This posed me with three problems:

  1. how to preserve the aspect ratio of the original images
  2. how to generate the thumbnails without tying up the application
  3. how to avoid running out of memory

Because generating thumbnails of images is (I think) of reasonably wide interest I thought I'd write it up as a code snippet. To recreate the project, start with a blank form and add two buttons, btnDetails and btnThumbs. Add a BackgroundWorker control named bgwThumbs. Add a ListView control and dock it to the bottom of the form. Add one column (Name) to the Details display.

The only thing you should have to change in the code is the value of the global variable, Path. Set this to the fully qualified path of a folder containing image files. The variable, Filter, is currently set to ignore all files except jpg, png and gif.

When you run the application you will see the list of files in the specified folder presented in Details view. Click on Thumbs to see the thumbnails. If you have a lot of files you will see the thumbnails being displayed as they are generated by the background thread. Because the generation is being done in the background you can switch between views at will. The thumbnail generation will not tie up the GUI.

Further comments on the code appear inline. Please feel free to comment and suggest improvements.

pritaeas commented: Nice one, rev. +14
'   Name:                                                                               
'       ThumbnailBG                                                                     
'   Description:                                                                        
'       Demo program that shows how to generate thumbnails which preserve the aspect    
'       ratio of the original image as well as how to do it from a background thread.   
'   Notes:                                                                              
'       Thumbnail size is defined by THUMBW and THUMBH.                                 
'       The ListView and ImageList controls cannot be accessed directly by the back     
'       ground thread. That requires the use of the structure, FileList, to provide the 
'       names of the files, and the delegate, dlgUpdateThumbs, to handle the updating   
'       as thumbs are generated.                                                        
'       Generating the thumbnails requires creating a bitmap for each source file. If   
'       the space for these bitmaps is not quickly reclaimed it can quickly lead to an  
'       out-of0memory error. To guard against this, the background thread explicitly    
'       disposes of bitmaps as soon as they are no longer required, and forces the      
'       system to do a garbage collection after every tenth thumbnail is created.       
'   2014-01-07  rj  original code                                                       

Imports System.Drawing
Imports System.Drawing.Drawing2D

Public Class Form1

    Private Path As String = "D:\My Documents\My Pictures\Current"
    Private Filter() As String = {"*.jpg", "*.png", "*.gif"}

    Private FileList As New Dictionary(Of Integer, String)

    'When a ListView is in icon view, images for each item are displayed based  
    'on images in an ImageList. a ListView can reference separate ImageLists    
    'for both small and large icons. For this demo I use only LargeIcons        

    Private imgFiles As New ImageList

    Const THUMBW = 120      'thumbnail width    
    Const THUMBH = 90       'thumbnail height   

    Private Delegate Sub dlgUpdateThumbs(i As Integer, thumb As Bitmap)

    Private Sub Form1_FormClosing(sender As Object, e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing

        'terminate the background worker thread


    End Sub

    Private Sub Form1_Load(sender As Object, e As System.EventArgs) Handles Me.Load

        'set properties of image list

        imgFiles.ImageSize = New Size(THUMBW, THUMBH)
        imgFiles.ColorDepth = ColorDepth.Depth16Bit

        'load the file list then start the BG process to generate thumbs

        lvwFiles.Items.Clear()                          'clear out any old items        
        lvwFiles.View = View.Details                    'default view is Details        
        lvwFiles.Columns(0).Width = lvwFiles.Width      'size first column to full width
        lvwFiles.LargeImageList = imgFiles              'link to ImageList              

        Dim i As Integer = 0

        For Each file As String In My.Computer.FileSystem.GetFiles(Path, FileIO.SearchOption.SearchTopLevelOnly, Filter)
            FileList.Add(i, file)
            i += 1

        'start the thumbnail generation

        bgwThumbs.WorkerSupportsCancellation = True

    End Sub

    Private Sub UpdateThumbs(i As Integer, thumb As Bitmap)

        'imgFiles and lvwFiles are updated with imformation coming from the background thread   
        'so we require a delegate. InvokeRequired will tell us if this sub is being called from 
        'the main thread or the background thread. The Try/Catch is required in case the user   
        'has decided to close the application. If this happens then Invoke will throw an error. 

        If lvwFiles.InvokeRequired Then
            'call was from the background thread - use the delegate
                Dim dlg As New dlgUpdateThumbs(AddressOf UpdateThumbs)
                Invoke(dlg, i, thumb)
            End Try
            'call was from the foreground (main) thread
            lvwFiles.Items(i).ImageIndex = i
        End If

    End Sub

    Private Sub btnDetails_Click(sender As System.Object, e As System.EventArgs) Handles btnDetails.Click

        'Put the display into Details view. Because there is only one column, size it to the    
        'width of the control.                                                                  

        lvwFiles.View = View.Details
        lvwFiles.Columns(0).Width = lvwFiles.Width

    End Sub

    Private Sub btnThumbs_Click(sender As System.Object, e As System.EventArgs) Handles btnThumbs.Click

        'Puut the display into LargeIcon view. Thumbnails will appear on the display as they
        'are generated.                                                                     

        lvwFiles.View = View.LargeIcon

    End Sub

    'Everything from here down belongs to the background thread

    Private Sub bgwThumbs_DoWork(sender As System.Object, e As System.ComponentModel.DoWorkEventArgs) Handles bgwThumbs.DoWork

        'Generate a thumbnail for each file in the list

        For Each i As Integer In FileList.Keys

            'CancellationPending is triggered by code in the FormClosing event handler  

            If bgwThumbs.CancellationPending Then
                e.Cancel = True
                Exit For
            End If

            'Generate two bitmaps. srce is from the original file and dest is srce      
            'resized (proportionally) to fit within the desired thumbnail size.         

            Dim srce As New Bitmap(Image.FromFile(FileList(i)))
            Dim dest As Bitmap = ResizeImage(srce, imgFiles.ImageSize.Width, imgFiles.ImageSize.Height)

            UpdateThumbs(i, dest)

            'dispose of the bitmaps and do a garbage collection after every ten images  


            If imgFiles.Images.Count Mod 10 = 0 Then GC.Collect()


    End Sub

    Public Function ResizeImage(srce As Bitmap, destW As Int32, destH As Int32) As Drawing.Bitmap

        'Resize a bitmap to fit with a given width and height. Scaling is done in a     
        'manner that preserves the original image's aspect ratio.                       

        Dim dest As New Drawing.Bitmap(destW, destH, Drawing.Imaging.PixelFormat.Format32bppArgb)

        Dim srceAspect = srce.Width / srce.Height
        Dim destAspect = dest.Width / dest.Height

        Dim NewX As Integer = 0                 'X offset of image in new bitmap
        Dim NewY As Integer = 0                 'Y offset of image in new bitmap
        Dim NewW As Integer = dest.Width        'width of image in new bitmap   
        Dim NewH As Integer = dest.Height       'height of image in new bitmap  

        'calculate sizes and offsets for centering

        If destAspect > srceAspect Then
            NewW = Convert.ToInt32(Math.Floor(srceAspect * NewH))
            NewX = Convert.ToInt32(Math.Floor((dest.Width - NewW) / 2))
            NewH = Convert.ToInt32(Math.Floor((1 / srceAspect) * NewW))
            NewY = Convert.ToInt32(Math.Floor((dest.Height - NewH) / 2))
        End If

        'draw the srce image into the dest bitmap

        Using grDest As Graphics = Graphics.FromImage(dest)
            grDest.CompositingQuality = CompositingQuality.HighQuality
            grDest.InterpolationMode = InterpolationMode.HighQualityBicubic
            grDest.PixelOffsetMode = PixelOffsetMode.HighQuality
            grDest.SmoothingMode = SmoothingMode.AntiAlias
            grDest.CompositingMode = CompositingMode.SourceOver
            grDest.DrawImage(srce, NewX, NewY, NewW, NewH)
        End Using

        Return dest

    End Function

End Class
oussama_1 39 Posting Whiz in Training

try adding the windows Explorer Browser, its much faster itll do the job.. just add your extension filter to it and set it to thumbnails.
download Microsoft.WindowsAPICodePack.Shell.dll
gd luck

Reverend Jim 4,009 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

The problem is that the Windows Explorer browser doesn't do what I want. My photos are in several dozen nested folders and each photo is named as

yyyy-mm-dd hh-mm tag tag tag ...

Where the date and time was automatically added from embedded EXIF data, and tags were added manually using another app that I wrote. My custom photo browser builds an internal list of all image files under

My Documents\My Pictures

and presents me with a textbox where I can enter tags. I click either the ALL or ANY button to select files with either ALL or ANY of the tags. Matching files are displayed in a listview. I click on a file to display a scaled image in a picture box. Using this app, I (and more importantly, my wife) can find specific photos in seconds. This is much faster than using Explorer.

Besides, this was more fun ^_^

oussama_1 39 Posting Whiz in Training

ok now i get it.
if you add the photos to the app resources, will that work for you ?

Reverend Jim 4,009 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

What would be the point of doing that?

oussama_1 39 Posting Whiz in Training

so that you can populate your listview with the images from your resources
i did this in one of my apps it was embeded with more than 100 photos but i was showing them in a picturebox(never tried listview) and i did a custom browser (listview) that loads icons from my resources it was really fast..
my point is, with the resources you can show the pictures instead of loading them.
btw i did try your code, nice work but i cant suggest anything bcz i got a fast pc,the code was working perfectly on it it was fast no generated the photos in no time (more than 80 photos about 4 mb each)
gd luck

Reverend Jim 4,009 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

Nice to hear it works well on a fast PC. I'll have to get me one. Mine is a 2008 Dell Inspiron 1720. Peppy at the time but a little slow in comparison with the new stuff.

John_260 0 Newbie Poster

Hey Reverend Jim,

Really like this project and built it from your code! However, I only managed to get the project up and running in VS 2015 after commenting out the following line of code: lvwFiles.Columns(0).Width = lvwFiles.Width (i.e. Form Load, btnDetails)??? This line causes an OutOfRange exception even when I substitute lvwFiles.Width for a value greater than the thumbnail width (i.e. 150, 200, 300, 400, 500) and made the form wider??? I am working in I'm looking for better ways to implement ListView as a thumbnail viewer in my image viewer project. But, although I have looked at a number of different projects, I just can't seem to get working what you have accomplished here? I already have an image viewer that opens images from file using FolderBrowserDialog and also creates thumbnails for a thumbnail slider (i.e. under the main image viewer picturebox). What I'm trying to do is call a separate form with exactly the data you have in your viewer (i.e. thumbnails and image name) as a larger thumbnail viewer (i.e. thumbnail slider is just a single row of thumbnails to click and view image). I would like to use the same thumbnails already created for the thumbnail slider and display them together with the image name in the larger thumbnail viewer when user clicks a button. But, so far, all I get is the image path (i.e. currently using filepath not filename)? Any ideas?

Reverend Jim 4,009 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

I haven't had a look at this code in some time. After I discovered the excellent free utility Everything Indexer I no longer needed it. Everything does, well, everything I had coded and so much more. It integrates with Explorer and has almost instantaneous response.

mariopepper 0 Newbie Poster Banned

I guess you have already solved that :D After 6 years

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, learning, and sharing knowledge.