Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Choose Images - Select Photographs from Camera or Memory Card

5.00/5 (3 votes)
30 Jul 2024CPOL36 min read 3.5K   161  
Choose Images is a program for photographers to help them review and select photographs from their camera or memory card to move to their computer for processing and/or archival purposes.
This is a rewrite of a program I submitted to Code Project in January 2016. The new version adds support for RAW files. It uses Phil Harvey's ExifTool command line/Perl utility to extract preview images and EXIF information from RAW files. It uses MetadataExtractor to extract EXIF tags and values from the extracted preview images and JPG images. It uses NewtonSoft.JSON to simplify storing EXIF tags in Settings. The biggest change was to display a set of thumbnails for the images in the folders being reviewed.

Sample Image - maximum width is 600 pixels

Introduction

Choose Images is an updated version of Photo Selector. I submitted an article describing Photo Selector to Code Project back in 2016. I've been using that program until now. Then I started a project to add support for RAW files. This turned out to be a complete rewrite as I added more capabilities and learned more about dealing with RAW files. I also changed the name to Choose Images to save confusion.

Choose Images is a an image processing program designed to simplify the process of selecting and copying JPG and RAW photographs from external media to a working folder on your hard drive. It can also be used on files on your hard drive for tasks like reviewing old files and folders for images that can be deleted or archived. Your processing work flow would start with the images you have selected and copied to a folder on your hard drive. Choose Images gets you to that point by making it easy to preview photographs and decide which ones are worth keeping and processing. Typically, you would check composition, aesthtics and sharpness.

In this era, when cameras can shoot dozens of 60-megapixel images in a second, just picking out the photos you want to keep after a photo outing can be a dauting task. One way Choose Images helps is by letting you zoom in on a particular area of an image. It then zooms in on the same area when you navigate to the next image. If you have taken a burst of shots of a football player catching a ball, or an Osprey catching a fish, for example, then the ability to quickly find the best shots in the sequence is very helpful.

Choose Images can display EXIF information for each image. You can configure the EXIF tags that it displays and you can even select different configurations for different camera brands, even when you are dealing with JPG files.

Credits

ExifTool by Phil Harvey

ExifTool provided the ability to extract EXIF information and JPG Preview files from almost any RAW images. It meant Choose Images could be used by RAW only photographers and it also provided performance benefits when dealing with high megapixel cameras.

NewtonSoft.JSON by James Newton-King

NewtonSoft.JSON provided an easy way to store and retrieve EXIF tag settings from Settings

MetadataExtractor by Drew Noakes

MetadataExtractor allowed Choose Images to extract all possible EXIF tags and values from JPG files, including the JPG files extracted from RAW files by ExifTool

ChatGPT by OpenAI

ChatGPT became an invaluable coding assistant. It saved me many hours of searching and helped me find better ways to do complex tasks.

Visual Studio 2022 Used to build Choose Images.

Paint.Net Used to create buttons and screen shots.

Performance Considerations

In 1998, at the dawn of the digital camera age, Sony introduced the Sony Mavica MVC-FD71 digital camera. It produced 640x480 byte images (307 kilobytes) and took 8 seconds to save a photo to its 1.44-megabyte floppy disk. In 2024 my Sony A7CR produces 9568x6376 byte images (61,000 kilobytes) and can save them to a 128,000 megabyte SD card fast enough that I can shoot 10 photos in a 1 second burst. The only reason I can shoot a burst is that the shots are buffered inside the camera's memory and written to the SD card over a longer period. Shoot enough bursts and the buffer fills up and I can't shoot another burst until the buffer clears.'

What held the Mavica back is what still holds digital cameras back today - the relatively slow performance of the camera's storage media compared to the camera itself, and the computers where we process and store digital images.

It wouldn't be a problem if digital images were still 640v480 bytes, but they are now 100 to 200 times larger. There is some partial good news in that you can configure your camera to produce much smaller JPG images. On my cameras, small low quality JPG images average 2.7 megabytes (full-frame) and 1.5 megabytes (APS-C). You can also extract smaller JPG Images from RAW camera files that are larger than the standard 160px x 120px thumbnail.

If you are a casual photographer and take a few dozen photos on on most outings, and maybe a couple of hundred photos on a vacation, then you can easily process and share the best of those images with your existing workflow. But, if you shoot wildlife, sports events and other activities, for example, you can end up with hundreds or thousands of photos to sort though. One reason why photographers do take so many photos is because they now can. In the old days of film, they would spend all their time loading and unloading 36 frame film cartridges if they tried to shoot bursts of photographs. If they were hoping to catch the critical moment when the bat hits the ball, the eagle grabs the fish, the runner crosses the finish line or the groom kisses the bride, they had to rely on their skill and judgement to capture those shots. Today, they can focus on the batter and, when he starts swinging, they can press and hold the shutter button and end up with a dozen or more photos with a high probability that one of them will be the shot where the ball flies off his bat and the sweat flies off his brow.

Choose Images was created to make it easier to quickly sort though large numbers of photographs before they end up clogging your computer's hard drive. Generally, you can tell fairly quickly whether a photograph makes the cut and is allowed onto your hard drive for further processing and evaluation. You don't really need a large version of the photograph to do that. While the 640x480 byte images from the Sony Mavica might be enough, you would probably like an image closer to the size of your monitor - say 1920x1200 bytes.

Let's test just how big a preview image needs to be. I shot the same scene in good light using all the combinations of JPG size and quality available on my Sony A7CR. These are the file sizes I obtained.

JPG Size vs Quality

Table 2. JPG Size and Quality Settings (Sony A7CR)

I used a program I wrote to compare images side-by-side to compare the highest quality large JPG image against the lowest quality small JPG image. Here is the original image and a side-by-side comparison of a cropped area of each image. In case you are wondering, the crop on the left is from the highest quality original.

Comparison Image

Cropped Comparison

From this comparison, we can see that a 4MB (around 2400x1600px) image is good enough for preview purposes. We can now figure out the fastest way to get similarly sized images onto a computer screen so we can review them. This depends on your preferred choice of image type for shooting.

JPG shooters

If you shoot JPG only, you want to strike a balance between size, quality and how the photos will be used. If you are sharing your photos on Instagram, you would choose the smallest and lowest quality JPG setting available to you. If you are sharing screen sized versions of your photos that people will enjoy on large screens such as computer monitors, high-definition TVs and tablets, then middle size and standard quality JPG files would be a good choice. If you are going to produce large prints to hang on a wall then full sized and high-quality images would be the best choice. In reality, most JPG shooters use the camera default JPG size and quality, and may adjust the quality setting "to get better pictures".

RAW Shooters

If you shoot RAW only you have some choice about the size. Some camera models offer RAW and compressed RAW. Some will deliver smaller RAW files, but only if you shoot in APS-C mode. What is not generally realized is that RAW files usually contain embedded JPG files of various sizes, ranging from thumbnail (160px x 120 px) up to full size. This varies by maker, but every one I've checked has an embedded JPG larger than a the thumbnail and smaller than full-size. If we can extract these files, they will be an excellent choice for image preview.

The following table illustrates some preview file sizes for popular camera brands.

Screen Shot

Table 1. Typical Preview File Sizes.

Luckily, I discovered ExifTool by Phil Harvey. That provided the ability to extract thumbnails and preview JPG files from almost all known RAW file formats. If you want to use Choose Images to preview RAW files, you must download ExifTool.exe and ExifTool.config from the ExifTool website and install in the same folder as Choose Images's executable. Be aware that you also need to install the exiftool_files folder in the same folder.

Initially, I was using a C# wrapper for ExifTool to extract EXIF data directly from RAW files. This worked but it meant reading large RAW files twice, once to get the Preview File and again to get the EXIF data. I suggested to Phil Harvey that it would be a nice feature for ExifTool to copy EXIF data to the preview files it extracted from RAW files. He kindly agreed and ExifTool Version 12.84 does that.

Choose Images uses ExifTool to read RAW files and extract embedded JPG files along with the original EXIF information. It does this quite quickly and the resulting Preview files can be treated like any other JPG images. Once you have selected the images you want to process, Choose Images can copy the matching RAW images to your computer. You can then use your favorite RAW processing software to save them for any purpose ranging from social media posts up to giant prints. You can reduce the time it takes to extract preview files and create thumbnails by using the Settings dialog to set an upper limit on the size of the Preview files extracted from RAW files. Table 1. shows the choices you can make.

In testing a folder containing 308 RAW files from my Sony A6700, I have two choices - Preview files that are 6192px x 4128px and preview files that are 1616px x 1080px. By setting the upper limit to 2000px, I forced Choose Images to use the smaller files. The time it took for ExifTool to extract the Preview files went from 20 seconds to 7 seconds and the time it took to load the thumbnails and display them went from 93 seconds to 21 seconds.

By way of comparison, I took the final JPG images created from the RAW files and used that folder as my source folder. It took 90 seconds to create and display the thumbnails. In other words, if you don't mind working with smaller preview files, you can get working on them faster by opening the Settings dialog and choosing Preview and a smaller Preview size limit. The actual sizes you can choose will depend on your Camera make and model.

RAW Plus JPG Shooters

This combination gives you the most options.

You can save all the JPG images and then go back and save the RAW versions for images that need fixing. This option is attractive when you are shooting large events and want to publish the photos very quickly. If you have the option, shooting small JPG images will speed up processing by a factor of 2 or 3.

You can shoot small JPG images and save the RAW versions of images you want to keep and process further.

You can ignore the JPG images and pick the smallest RAW preview size available and save the RAW versions of images you want to keep and process further.

Personally, I usually shoot small JPG and RAW. When I started shooting a DSLR, I only shot JPG. One day, I went with a friend to Magee Marsh to shoot warblers. She had been trying to convince me to shoot RAW to no avail. I came home with hundreds of photos. They looked fine on the LCD screen. When I copied them to my computer, I was horrified to find that every photo was a garish mess. I had accidentally set my camera to shoot in Comic mode where the images were transformed to look like cartoons. They were lousy cartoons and even worse bird photographs, and I had no way to fix them. Had I shot RAW... After testing, I've concluded that on my cameras, shooting RAW and choosing small Preview images would be the best option for me.

Working with RAW files

The embedded JPG preview files, even the ones as small as those measuring 1616px x 1080px from older Sony models, are perfectly adequate if your objective is to preview files. The much larger preview files from recent camera models are more than adequate. The embedded thumbnails, at 160px x 120px are not so useful for preview purposes.

In its Settings dialog, Choose Images lets the user specify the longest dimension allowed in a preview file and whether to create thumbnails and preview images from the JPG files in the source folder or from the preview JPG files extracted by ExifTool. If you limit the size of preview JPG files and use them instead of the original JPG images, you can get the best performance for loading and displaying thumbnails.

Choose Images does not know which size of preview images are available in a particular set of images. I thought about configuring this by brand, but that would not have been a good choice. The reason is that different models save different sets and sizes of preview images. You can see this from Table 1. So Choose Images tries to determine which one to use by checking the first image in the source folder. The following method does that:

C#
private string GetPreviewType(string sampleRawFile, string tempFolder) {

    // Look for the preview type that does not exceed the configurable upper limit on the longest dimension
    List<string> previewTypes = new List<string>() { "PreviewImage", "JpgFromRaw", "OtherImage", "ThumbnailImage" };
    List<int> previewSizes = new List<int>() { 0, 0, 0, 0 };

    int bestSize = 0;
    string bestType = null;
    string args = "-preview:All -a -b -W " + tempFolder + _PathSeparator + "/%f_%t%-c.%s \"" + sampleRawFile + "\"";
    ExecuteExifTool(args, 500);
    for (int i = 0; i < previewTypes.Count; i++) {
        string previewType = previewTypes[i];
        string previewFileName = Path.GetFileNameWithoutExtension(sampleRawFile) + "_" + previewType;
        string previewPath = Path.Combine(_TempFolderPath, previewFileName + ".JPG");
        if (File.Exists(previewPath)) {
            var dimensions = GetImageDimensions(previewPath);
            int maxDim = Math.Max((int)dimensions.Width, (int)dimensions.Height);
            previewSizes[i] = maxDim;

            // Check maximum dimension against the maximum
            if (maxDim <= _MaxPreviewDimension && maxDim > bestSize) {
                bestSize = maxDim;
                bestType = previewType;
            }
        }
    }

    // It may happen that your folder on a hard drive contains photos from multiple cameras with different
    // maximum preview sizes. If the largest preview size is selected, signal ExifTool to get the largest
    // Preview image it finds in each image.
    if (_SDCardDrive != null) {
        int maxIndex = previewSizes.IndexOf(previewSizes.Max());
        if (previewTypes[maxIndex] == bestType) {
            return "All";
        }
    }
    return bestType;
}
private (int Width, int Height) GetImageDimensions(string imagePath) {
    using (Image image = Image.FromFile(imagePath)) {
        return (image.Width, image.Height);
    }
}

Here is an example of the ExifTool command used to extract all the JPG Preview files from a selected RAW file folder.

-preview:All -a -b -W C:\Users\patjd\AppData\Local\Temp\ZZZTmp10040622\/%f_%t%-c.%s "L:\DCIM\10040622\PJD06937.ARW"\

Choose Images invokes ExifTool and passes a command that extracts the preview files from a folder of RAW files and copies them to a temporary directory. The EXIF data is embedded in the files, so there is no need to read EXIF data from the original RAW image. From that point, Choose Images can process these files just like a it would a folder of JPG files taken directly from the camera. Here's an example of the command passed to ExifTool after it has determined that it should extract PreviewImage JPG files.

-preview:PreviewImage -a -b -w C:\Users\patjd\AppData\Local\Temp\ZZZTmp11340601/%f.JPG "L:\DCIM\11340601\*.ARW"

If the selected preview type is also the largest, Choose Images uses an ExifTool command which will extract the biggest type of preview file from each file. This is a ""cannot happen"" situation on camera media but may happen if you are looking at an archival folder that contains images from multiple cameras.

-config .ExifTool.config -p "${BigImage;SetTags('all:all#')}" -b -w C:\Users\patjd\AppData\Local\Temp\AAATmp10240427/%f.jpg "L:\DCIM\10240427\*.ARW"

The ExifTool.config argument refers to a file used by ExifTool. In this case it contains the definition for BigImage which tells ExifTool to select the largest of the preview images found in a RAW file. The first folder is where the preview files are to be written. The second is the source for the RAW files - Sony RAW files in this case. You may notice that the two directory names contain the same number. This is because Choose Images stores thumbnails and preview images in a temporary folder named after the source folder located in the Windows designated Temp folder.

C#
private void ExecuteExifTool(string arguments, int timeOut) {

    string path = AppDomain.CurrentDomain.BaseDirectory;
    string command = Path.Combine(path, "ExifTool.exe");

    // Create a ProcessStartInfo object
    ProcessStartInfo processStartInfo = new ProcessStartInfo(command, arguments)
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true
    };

    // Create and start the process
    using (Process process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }) {
        process.Exited += Process_Exited; // Subscribe to the Exited event
        process.Start();

        // Read the output streams asynchronously
        string output = process.StandardOutput.ReadToEnd();
        string error = process.StandardError.ReadToEnd();

        // Wait for the process to exit with a timeout
        if (!process.WaitForExit(timeOut)) {
            process.Kill();
            Log("ExifTool process killed due to timeout.", LogType.Error);
        }

        // Close the process and read output/errors
        process.Close();

        if (!string.IsNullOrEmpty(error)) {
            Log("exifttol error:" + error, LogType.Error);
        }
    }
}

Opening Source Folders

Choose Images is designed to copy selected files from external media, such as an SD card, to your hard drive.

The first step in using Choose Images is selecting where the source files are located. You can use the File Menu to open the folder where the source files are stored. You can also use the Recent Source Folders menu item to open folders you've used before. Choose Images does not recurse through subfolders. Choose Images then determines the earliest and latest dates of the files in the selected folder. If the date range is more than a day, it prompts you to select a date range.

Screen Shot of Date Range Selector

The code to determine that date range was largely created by ChatGPT. Here's what it gave me.

C#
private Dictionary<string, DateTime> GetFileDateRange(string sourceFolder) {
    Dictionary<string, DateTime> fileDateRange = null;
    string[] extensions = { ".JPG", "." + _RAWExtension };
    ConcurrentDictionary<string, DateTime> fileLastWriteTimes = new ConcurrentDictionary<string, DateTime>();

    Parallel.ForEach(Directory.EnumerateFiles(sourceFolder, "*.*", SearchOption.TopDirectoryOnly)
                                .Where(file => extensions.Contains(Path.GetExtension(file).ToUpper())),
    file =>
    {
        DateTime lastWriteTime = File.GetLastWriteTime(file);
        fileLastWriteTimes.TryAdd(file, lastWriteTime);
    });

    // Convert to list for further processing
    List<KeyValuePair<string, DateTime>> fileList = fileLastWriteTimes.ToList();

    // Sort by creation time
    fileList.Sort((x, y) => DateTime.Compare(x.Value, y.Value));

    // Get the last write time of the first and last photow
    if (fileList.Count > 0) {
        fileDateRange = new Dictionary<string, DateTime>();
        fileDateRange.Add(fileList.First().Key, fileList.First().Value);
        fileDateRange.Add(fileList.Last().Key, fileList.Last().Value);
    }
    return fileDateRange;
}

Choose Images scans the folder you select to discover whether they contains RAW or JPG images or both. If classifies the folder as being JPG only, RAW only or a combination of both. In most cases, folders that contain both RAW and JPG folders will have files that share the same name but differ in extension. This is the first part of the code to do this classification. Note that there is a setting that tells Choose Images whether to load thumbnails and EXIF information from JPG files or from the preview JPG files extracted from RAW files by ExifTool. If you are happy with reviewing files that are around the size of a typical monitor, then choosing the latter option along with setting the maximum dimension of a preview file to 2000px will deliver better performance than using JPG files directly. As always, your mileage may vary.

C#
private SourceFolderType GetSourceFolderInfo(string sourceFolder) {

    // Create a DirectoryInfo object for the folder
    DirectoryInfo directoryInfo = new DirectoryInfo(sourceFolder);

    // Check if the folder exists
    if (directoryInfo.Exists) {

        // Get all the JPG files in the folder
        FileInfo[] jpgFiles = PruneByDateRange(directoryInfo.GetFiles("*.JPG", _SearchOption));

        // Get all the RAW files in the folder
        FileInfo[] rawFiles = PruneByDateRange(directoryInfo.GetFiles("*." + _RAWExtension, _SearchOption));
        if (rawFiles.Length > 0) {
            _RAWSourceFolder = sourceFolder;
        }

        if (jpgFiles.Length == 0 && rawFiles.Length > 0) {
            _AllFilesInfo = rawFiles;
            return SourceFolderType.RAW;

        }
        if (jpgFiles.Length > 0 && rawFiles.Length == 0) {
            _AllFilesInfo = jpgFiles;
            return SourceFolderType.JPG;
        }
        jpgFiles = jpgFiles.OrderBy(file => file.Name).ToArray();
        rawFiles = rawFiles.OrderBy(file => file.Name).ToArray();
        if (jpgFiles.Length == rawFiles.Length) {
            bool match = true;
            for (int i = 0; i < jpgFiles.Length; i++) {
                if (Path.GetFileNameWithoutExtension(jpgFiles[i].Name) != Path.GetFileNameWithoutExtension(rawFiles[i].Name)) {
                    match = false;
                    break;
                }
            }
            if (match) {
                if (_JPGorPreview == "JPG") {
                    _AllFilesInfo = jpgFiles;
                    return SourceFolderType.JPG;
                }
                else {
                    _AllFilesInfo = rawFiles;
                    return SourceFolderType.RAW;
                }
            }
        }
        ...

Things get a little complicated if the JPG files and RAW files don't match up, either in file names and/or count. This could happen if you start shooting JPG only and then switch to shooting RAW or RAW plus JPG. In this case, Choose Images decides whether to go with RAW or JPG and lists the files it can't match on an Unmatched Files form. You then have three options: ignore these files, move them to another folder or copy them to another folder. If the files are on an SD card, it is probably best to copy them to avoid in-camera issues with files being deleted outside of the camera. At least, that is the case with Sony cameras. The code continues to resolve what sort of files we are dealing with:

C#
         ...
         else if (jpgFiles.Length > rawFiles.Length) {
            // Extract the base file names (without extensions) from the rawFiles array
            var rawFileBaseNames = new HashSet<string>(rawFiles.Select(file => Path.GetFileNameWithoutExtension(file.Name)));

            // Filter the jpgFiles array to exclude files with matching base names in rawFiles
            FileInfo[] prunedJpgFiles = jpgFiles
                .Where(file => !rawFileBaseNames.Contains(Path.GetFileNameWithoutExtension(file.Name)))
                .ToArray();

            if (prunedJpgFiles.Length > 0) {
                frmProcessUnmatchedFiles frmPU = new frmProcessUnmatchedFiles() { Files = prunedJpgFiles, Folder = _DestinationJPGFolder };
                if (frmPU.ShowDialog() == DialogResult.OK) {
                    _AllFilesInfo = rawFiles;
                    return SourceFolderType.RAW;
                }
            }
        }
        else if (jpgFiles.Length < rawFiles.Length) {
            // Extract the base file names (without extensions) from the rawFiles array
            var jpgFileBaseNames = new HashSet<string>(jpgFiles.Select(file => Path.GetFileNameWithoutExtension(file.Name)));

            // Filter the jpgFiles array to exclude files with matching base names in rawFiles
            FileInfo[] prunedRAWFiles = rawFiles
                .Where(file => !jpgFileBaseNames.Contains(Path.GetFileNameWithoutExtension(file.Name)))
                .ToArray();

            if (prunedRAWFiles.Length > 0) {
                frmProcessUnmatchedFiles frmPU = new frmProcessUnmatchedFiles() { Files = prunedRAWFiles, Folder = _DestinationRAWFolder };
                if (frmPU.ShowDialog() == DialogResult.OK) {
                    _AllFilesInfo = jpgFiles;
                    return SourceFolderType.JPG;
                }
            }
        }
    }
    return SourceFolderType.Undefined;
}

Selecting Thumbnails

Most cameras embed a 160px by 120px thumbnail in the JPG and RAW images they generate. These thumbnails are too small for review purposes. For example, you can't distinguish a shot where the subject is in focus from another where the camera focused on something in front or behind the subject. Therefore, Choose Images creates larger thumbnails from JPG images. These will be resized copies of the JPG files produced by the camera or the JPG images extracted from RAW images by ExifTool. The size of these thumbnails can be configured with the longest dimension limited to the range 50px to 360px. You can change the size at any time and the thumbnail display will be updated immediately. Choose Images saves a copy of each thumbnail at 360x240px in the temp folder associated with the source folder. If you reopen the source folder, it will use these thumbnail files for every matching JPG image. It also saves the Date Taken EXIF value for each file in a simple CSV file to save retrieving it again. Using the saved thumbnails and Date Taken list speeds up loading thumbnails a lot if you come back to a previously selected folder. The following method is used to load a Thumbnail image.

C#
private ThumbnailInfo GetThumbnailInfo(string path) {
    ThumbnailInfo thumbnailInfo = new ThumbnailInfo();

    string tempThumbNailPath = string.Empty;
    string pathName = Path.GetFileNameWithoutExtension(path);
    string fileDateCSV = Path.Combine(_TempFolderPath, "FileDate.csv");
    if (Directory.Exists(_TempFolderPath)) {
        tempThumbNailPath = Path.Combine(_TempFolderPath, pathName) + ".THM.JPG";
    }

    try {
        if (File.Exists(tempThumbNailPath)) {
            using (FileStream fs = new FileStream(tempThumbNailPath, FileMode.Open, FileAccess.Read)) {
                // Load the image from the FileStream
                using (Image thumbnail = Image.FromStream(fs)) {
                    _BytesRead += thumbnail.Width * thumbnail.Height;
                    thumbnailInfo.Thumbnail = new Bitmap(thumbnail, _MaxThumbnailSize);
                }
                thumbnailInfo.DateTaken = _TempFileDates[pathName];
            }
        }
        else if (File.Exists(path)) {
            using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) {
                // Load the image from the FileStream
                using (Image thumbnail = Image.FromStream(fs)) {
                    _BytesRead += thumbnail.Width * thumbnail.Height;

                    // Resize the thumbnail to the maximum thumbnail size
                    thumbnailInfo.Thumbnail = new Bitmap(thumbnail, _MaxThumbnailSize);
                }
                thumbnailInfo.DateTaken = GetDateTakenFromJPGImage(fs);
                if (tempThumbNailPath != string.Empty) {
                    using (StreamWriter sw = File.AppendText(fileDateCSV)) {
                        sw.WriteLine(pathName + "," + thumbnailInfo.DateTaken.ToString());
                    }
                    SaveImageToFile(thumbnailInfo.Thumbnail, tempThumbNailPath, ImageFormat.Jpeg);
                }
            }
        }
        return thumbnailInfo;
    }
    catch (Exception ex) {
        // Handle the exception as needed (e.g., log the error, continue with the next image)
        MessageBox.Show(_Messages[15].Replace("<filename>", path) + ex.Message, "Error");
    }
    return null;

If you click on a thumbnail, the image will be displayed at the current zoom level. The default is "Fit to Window". You can zoom from 10% of the original image size up to 100% in steps of 10% using keyboard shortcuts or the combo box on the tool bar. These preview images let you check focus and other considerations such as composition, bokeh, distractions and appeal. If you are satisfied with the image you can copy it to a specified destination folder for further processing. You can also mark it as selected and defer the decision to copy it until you have reviewed more photos.

Once the _AllFileInfo array of FileInfo objects has been created, you can remove any duplicates. It is unlikely you would have duplicates on an SD card but you could in a large archive folder of RAW files, for example. All Choose Images requires from an image file is a copy of the image large enough for preview purposes - say 1500px x 1000px - and the EXIF information. If the source files are RAW, it uses ExifTool to extract preview images that contain the original EXIF data. These preview images are stored in a temporary folder and used in place of the actual RAW files. The paths to the files to be reviewed are stored in List<string> _FileList</string>.

Choose Images needs a way to store information about each file that makes it easy to create and manipulate a set of thumbnail images. It uses the following objects to do that.

C#
// Thumbnail Dictionary Declarations
private enum ImageFlag {
    NotInDateRange,
    None,
    Checked,
    Deleted,
    Copied,
    CheckPending,
    UncheckPending
}

private class ThumbnailInfo : IDisposable {
    public int Index { get; set; }
    public Image Thumbnail { get; set; }
    public DateTime DateTaken { get; set; }
    public ImageFlag Flag { get; set; }
    public void Dispose() {
        Dispose();
        GC.SuppressFinalize(this);
    }
}
private readonly Dictionary<string, ThumbnailInfo> _Thumbnails = new Dictionary<string, ThumbnailInfo>();

Thumbnails is indexed by the file path. In the case of RAW source images, this is the path to the preview JPG.
Thumbnail.Index gives the position of the corresponding image in _FileList, a list of all the image files selected for processing.
Thumbnail.Image is the thumbnail image resized to 360px x 240px, the maximum thumbnail size that Choose Images displays.
Thumbnail.ImageFlag gives the current state of the thumbnail according to enum imageFlag.

Because the ThumbnailInfo objects contain images, we declared them disposable. The Thumbnail dictionary is populated by reading all the JPG images or JPG preview images referenced in _FileList and extracts a 360px x 240px thumbnail image from each file. It also extracts the "Date Taken" EXIF tag value so images outside a selected Date Range can be ignored. This might be because you have month''s worth of photos on your SD card folder but you only want to review the ones from the week you were away in Yellowstone, for example.

This method is used to populate entries in the _ThumbnailInfo dictionary:

C#
private ThumbnailInfo GetThumbnailInfo(string path) {
    ThumbnailInfo thumbnailInfo = new ThumbnailInfo();
    try {
        if (File.Exists(path)) {
            using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) {
                // Load the image from the FileStream
                using (Image thumbnail = Image.FromStream(fs)) {
                    _BytesRead += thumbnail.Width * thumbnail.Height;

                    // Resize the thumbnail to the maximum thumbnail size
                    thumbnailInfo.Thumbnail = new Bitmap(thumbnail, _MaxThumbnailSize);
                }
                thumbnailInfo.DateTaken = GetDateTakenFromJPGImage(fs);
            }
            return thumbnailInfo;
        }
    }
    catch (Exception ex) {
        // Handle the exception as needed (e.g., log the error, continue with the next image)
        MessageBox.Show(_Messages[15].Replace("<filename>", path) + ex.Message, "Error");
    }
    return null;
}

Populating the dictionary requires a pass through all the files selected as source files. This is expensive in time, since Choose Images may be reading hundreds or even thousands of large JPG files from relatively slow media. When I was demonstrating Choose Images to my wife and chose a large source folder on an SD card to preview, she asked "Why am I looking at a blank screen?" Obviously, my little progress bar was providing insufficient feedback. I updated Choose Images to display the thumbnails as the files were being read. Despite updating a relatively large image for every file read, the performance impact was negligible. If the spread of thumbnailInfo.DateTaken dates exceeds a day, then Choose Images shows a form that allows you to limit the date range, and thus the number of thumbnail images.

Image Flow and the Temporary Folder

Cropped Comparison

Displaying Images

Choose Images uses a picture box picMainImage nested inside a panel pnlMainFrame to display images and thumbnails. It is critical that pnlMainframe has its AutoScroll property set to true. This property means that the panel control will supply scroll bars if the image assigned to the Image property of picMainImage exceeds the bounds of pnlMainFrame. It also allows us to manipulate the AutoScrollPosition of the panel to scroll and pan the image.

You choose an image to view by clicking on a thumbnail. Once an image is chosen, you can navigate to the next or previous image using the arrow buttons on the tool bar or the left and right arrow keys on the keyboard while holding the Ctrl key. If the last image is displayed, then clicking the right button or arrow key will take you back to the first image and vice versa.

Arrow Buttons

Selecting an Image

Selection is indicated by a toggle at the right end of the toolbar. Clicking the toggle changes its state. The screenshots show it toggled off and on. When you click the Copy or Delete buttons while an image is being displayed, then Choose Images checks whether the image has been selectd. If it has not been selected then just the current image is copied or deleted. Otherwise, all selected images are copied or deleted, after which the images are deselected.

Unselected Image

Selected Image

Here is the method to read in an image from external media. It checks that the file is a valid JPG and not too large. That size is configurable, which is probably necessary as cameras move to ever higher megapixel counts. It also loads embedded EXIF data from the image file. If the file is not a valid JPG image, then an error image is displayed instead. The errant file name is written to that image.

C#
private Image GetImageFromMedia(string sourceImagePath) {
    Image jpgImage = null;
    bool validJPG = true;
    Dictionary<string, string> exifDictionary = new Dictionary<string, string>();

    // Load the image from the FileStream
    using (FileStream fs = new FileStream(sourceImagePath, FileMode.Open, FileAccess.Read)) {

        // Get the file size
        long fileSize = new FileInfo(sourceImagePath).Length;

        // Check if the file size exceeds the maximum allowed size
        if (fileSize > _MaxFileSize) {
            validJPG = false;
        }
        else {
            byte[] buffer = new byte[4];
            int bytesRead = fs.Read(buffer, 0, buffer.Length);

            // Check if the file has a valid JPG signature
            if (!(bytesRead >= 4 && IsJpgFileSignature(buffer))) {
                validJPG = false;
            }
        }
        if (validJPG) {
            // Reset the file position to the beginning
            fs.Seek(0, SeekOrigin.Begin);

            // Load the image from the FileStream
            jpgImage = Image.FromStream(fs);
            exifDictionary = LoadAllEXIFTagsFromJPGImage(fs);
            ExtractJPGExifInformation(exifDictionary);
            chooseEXIFTagsToolStripMenuItem.Enabled = true;
        }
        else {
            jpgImage = global::ChooseImages.Properties.Resources.InvalidJPG;
            using (Graphics g = Graphics.FromImage(jpgImage)) {
                AddFileNameToImage(sourceImagePath, 64, g, jpgImage);
            }
        }
    }
    return jpgImage;
}
...
bool IsJpgFileSignature(byte[] buffer) {
    // Check if the first bytes match the JPG file signature
    return buffer.Length >= 2 && buffer[0] == 0xFF && buffer[1] == 0xD8;
}

The following screen shot shows how Choose Images displays an image.

Screen Shot of a Sample Image

Zooming and Panning Images

The toolbar has a ComboBox that is used to set the zoom factor of displayed images. It has a default of "Fit to Window". At this zoom setting, Choose Images will resize the image if the Choose Images window is resized. The other zoom settings are 10% through 100%. They display the image resized to a percentage of the full resolution of the original image. If the images were loaded using previews from RAW images, then full resolution can vary a lot between cameras and models. You can change the zoom using the Zoom dropdown or by using the key combinations ctrl+1 (10%) through ctrl+0 (100%). You can use ctrl+= to get back to the default of "Fit to Window"

The primary mission of Choose Images is to help you select the photos you consider worth keeping and/or processing with your favorite image processing program, such as Lightroom, PhotoShop, DXO Photolab, Luminar, etc. One thing photographers want to see close-up is the subject of the photograph. Choose Images lets you double click on the subject and it will zoom to that point at full resolution. This saves a lot of scrolling and/or panning. Often, you may take a burst of shots to capture action. If you navigate to the next image after double-clicking, then that image will be zoomed to the same point in the image. This is useful when you have taken a burst of shots to capture fast moving sports or wild life action.

If the image is too large to fit the current dimensions of the panel control containing the host picturebox control, then scroll bars will be shown as required. You can scroll the image using these scroll bars. You can also scroll vertically using the mouse wheel. Theoretically, one should be able to scroll horizontally using the mouse wheel while holding down the shift key, but that doesn't seem to work properly. You can pan the image using mouse while holding down the left button. You can use the arrow keys and Home (scroll to top of image), End (scroll to bottom of image). PgUp (scroll up 90% of the current window size) and PgDn (scroll down 90% of the current window size).

The code to implement panning using the mouse follows.

C#
private void picMainImage_MouseDown(object sender, MouseEventArgs e) {

    if ((string)btnThumbnail.Tag == "View") {
        // Check if the left mouse button is pressed for panning
        if (e.Button == MouseButtons.Left) {
            // Record the starting position for panning
            _LastMousePos = e.Location;
            _PanScrollPosition = pnlMainFrame.AutoScrollPosition;

            // Start timer
            _DeltaX = 0;
            _DeltaY = 0;
            _Timer.Start();
        }
        return;
    }
    // Irrelevant code omitted
}
private void picMainImage_MouseMove(object sender, MouseEventArgs e) {

        // Irrelevant code omitted

        // Check if the left mouse button is pressed for panning
        if (e.Button == MouseButtons.Left) {
            // Calculate the panning offset
            int deltaX = mousePos.X - _LastMousePos.X;
            int deltaY = mousePos.Y - _LastMousePos.Y;

            // Accumulate the offset
            _DeltaX += deltaX;
            _DeltaY += deltaY;

            // Update last mouse position
            _LastMousePos = new Point(mousePos.X, mousePos.Y);
        }

        // Irrelevant code omitted
}
private void Timer_Tick(object sender, EventArgs e) {
    // Update the scroll position
    pnlMainFrame.AutoScrollPosition = new Point(-_PanScrollPosition.X - _DeltaX, -_PanScrollPosition.Y - _DeltaY);
    _PanScrollPosition = pnlMainFrame.AutoScrollPosition;
}
private void picMainImage_MouseUp(object sender, MouseEventArgs e) {

    if ((string)btnThumbnail.Tag == "View") {
        // Check if the left mouse button is pressed for panning
        if (e.Button == MouseButtons.Left) {
            _Timer.Stop();
            _ImageScrollPosition = pnlMainFrame.AutoScrollPosition;
        }
    }
}

The code to support scroll operations follows.

C#
private void ScrollImage(ScrollType scrollType) {
    int deltaX = 0;
    int deltaY = 0;
    int overlapX = (int)(pnlMainFrame.Width * 0.1);
    int overlapY = (int)(pnlMainFrame.Height * 0.1);

    // Calculate the new AutoScrollPosition
    switch (scrollType) {
        case ScrollType.ScrollUp:
            deltaY = pnlMainFrame.Height - overlapY;
            break;
        case ScrollType.ScrollDown:
            deltaY = -pnlMainFrame.Height + overlapY;
            break;
        case ScrollType.ScrollLeft:
            deltaX = pnlMainFrame.Width - overlapX;
            break;
        case ScrollType.ScrollRight:
            deltaX = -pnlMainFrame.Width + overlapY;
            break;
        case ScrollType.PageUp:
            deltaY = picMainImage.Height;
            break;
        case ScrollType.PageDown:
            deltaY = -picMainImage.Height;
            break;
        case ScrollType.PageLeft:
            deltaY = picMainImage.Width;
            break;
        case ScrollType.PageRight:
            deltaY = -picMainImage.Width;
            break;
        default:
            deltaX = 0;
            deltaY = 0;
            break;
    }

    // Delay setting the AutoScrollPosition until after the layout has been updated
    BeginInvoke(new Action(() =>
    {
        // Set the new AutoScrollPosition
        pnlMainFrame.AutoScrollPosition = new Point(-_ImageScrollPosition.X - deltaX, -_ImageScrollPosition.Y - deltaY);
        _ImageScrollPosition = pnlMainFrame.AutoScrollPosition;
    }));
}

I am indebted to ChatGPT for suggesting the use of a timer to update the scroll position as the mouse moves. That eliminated a nasty flickering issue. The Timer Tick interval can be set in settings if panning isn't responsive enough or is too responsive. This will depend on your hardware.

Keyboard Shortcuts

Developers sometimes develop an application that depends on a pointing device and forget about the keyboard except as an input device. However, keyboard support can make an application easier to use and save time. For example, it is nice to be able to pan an image by dragging with the mouse. But, if you have a large image, it becomes tiresome if you want to get to the bottom of the image. You could use the scroll bars, but they can be a bit finicky to grab and drag with a mouse. That's when you appreciate being abble to use the keyboards navigation keys, such as PgDn and End. Choose Images provides a full set of keyboard commands. You can see the supported shortcuts by choosing Help/Keyboard Shortcuts from the menu or by entering Alt+HK on the keyboard. This is a screenshot showing the keyboard shortcuts.

Keyboard Shortcuts

The code to handle all key combinations uses the ProcessCmdKey method. I was having trouble using the usual KeyUp, KeyDown, and KeyPress events to capture all the key combinations I wanted to use. I asked Chat GTP for help and it suggested using ProcessCmdKey, because it intercepts key strokes before any controls on the form grabs them. Here is the code to process key combinations, with obvious code omitted.

C#
protected override bool ProcessCmdKey(ref Message msg, Keys keyData) {

    // Intercepts all key strokes and we process those we have assigned functionality to.
    if ((string)btnThumbnail.Tag == _View) {
        switch (keyData) {

            // Vanilla key selected
            case Keys.Up:
                ScrollImage(ScrollType.ScrollUp);
                return true; // Indicate that the key has been handled
            case Keys.Down:
                ScrollImage(ScrollType.ScrollDown);
                return true;

            // Obvious code omitted

            // Ctrl + Key selected
            case Keys.Control | Keys.Left:
                MoveTo(FileListIndex.Previous);
                return true;
            case Keys.Control | Keys.Right:
                MoveTo(FileListIndex.Next);
                return true;
            case Keys.Control | Keys.Home:
                MoveTo(FileListIndex.First);
                return true;
            case Keys.Control | Keys.End:
                MoveTo(FileListIndex.Last);
                return true;
            // Zoom control
            case Keys.Control | Keys.Oemplus:
                // case Keys.Control | Keys.NumPad0:
                ProcessZoom(0);
                return true;
            case Keys.Control | Keys.D1:
            case Keys.Control | Keys.NumPad1:
                ProcessZoom(1);
                return true;

            // Obvious code omitted

            case Keys.Control | Keys.D9:
            case Keys.Control | Keys.NumPad9:
                ProcessZoom(9);
                return true;
            case Keys.Control | Keys.D0:
            case Keys.Control | Keys.NumPad0:
                ProcessZoom(10);
                return true;
        }
    }
    else if ((string)btnThumbnail.Tag == _Thumbnail) {
        switch (keyData) {

            // Vanilla key selected

            case Keys.Home:
                ScrollImage(ScrollType.PageUp);
                return true;
            case Keys.End:
                ScrollImage(ScrollType.PageDown);
                return true;
            case Keys.PageUp:
                ScrollImage(ScrollType.ScrollUp);
                return true;
            case Keys.PageDown:
                ScrollImage(ScrollType.ScrollDown);
                return true;
        }
    }
    // If not handled, pass the key to the base class
    return base.ProcessCmdKey(ref msg, keyData);
}

Displaying Thumbnails

Choose Images creates a single image from every individual thumbnail image stored in the Thumbnails Dictionary. This is the method code used to create that image.

C#
private Image CreateCompositeImage() {
    // Calculate the number of rows required
    int numRows = Math.Max(1, (int)Math.Ceiling((double)_Thumbnails.Count / _ThumbnailsPerRow));

    // Create a blank composite image
    Bitmap compositeImage = new Bitmap(_ThumbnailsPerRow * (_ThumbnailSize.Width + _BorderWidth * 2),
                                                 numRows * (_ThumbnailSize.Height + _BorderWidth * 2));

    using (Graphics g = Graphics.FromImage(compositeImage)) {
        foreach (KeyValuePair<string, ThumbnailInfo> kvp in _Thumbnails) {
            string fileName = kvp.Key;
            ThumbnailInfo thumbnailInfo = kvp.Value;
            Size thumbPos = ThumbPos(thumbnailInfo.Index);
            using (Image resizedThumbnail = new Bitmap(thumbnailInfo.Thumbnail, _ThumbnailSize)) {
                g.DrawImage(resizedThumbnail, thumbPos.Width, thumbPos.Height);
            }

            // Calculate the position for the current thumbnail inside the composite image
            Point point = GetThumbNailPosition(thumbnailInfo.Index);
            int x = point.X;
            int y = point.Y;
            if (thumbnailInfo.Flag == ImageFlag.Checked) {
                DrawCheckmark(g, x, y);
            }
            else if (thumbnailInfo.Flag == ImageFlag.Deleted) {
                DrawCross(g, x, y);
            }
        }
    }
    return compositeImage;
}
private Point GetThumbNailPosition(int fileIndex) {
    Point point = new Point();
    // Find the position of the clicked thumbnail inside the composite image
    int row = fileIndex / _ThumbnailsPerRow;
    int col = fileIndex % _ThumbnailsPerRow;

    // Calculate the position for the current thumbnail inside the composite image
    point.X = col * (_ThumbnailSize.Width + _BorderWidth + 1);
    point.Y = row * (_ThumbnailSize.Height + _BorderWidth + 1);
    return point;
}

The GetThumbNailPosition method is also used to figure out the X and Y coordinates of the thumbnail within the composite image. In this case, it is used to tell where to draw a checkmark or a cross in the corner of the thumbnail to reflect its state. Note that the code does not read image files from disk. If you decide to change the thumbnail size, then the CreateCompositeImage method is used to recreate the thumbnails almost instantly. This is how thumbnails look at maximum thumbnail resolution and minimum resolution.

Screen Shot of Thumbnails at maximum size

Screen Shot of Thumbnails at minimum size

Selecting Images

If you click on a thumnbnail, then the corresponding image is read from the media and displayed at the current zoom setting. If you ctrl+click a thumbnail, then the thumbnail is selected and a yellow checkmark is drawn in the bottom right corner. If you shift+click a thumbnail, then the selection is extended to the nearest selected thumbnail, and selected thumbnails outside the extended selection are unselected. The following sequence of screen shots illustrates selection.

Screen Shot of thumbnails selected with ctrl+click

Ctrl-clicked the first three thumbnails in the second row and the last two in the third row.

Screen Shot of thumbnails selected with shift+click

Shift-clicked the first thumbnail in the third row.

When you move the mouse over a thumbnail, the file name is displayed as a tool tip. This is the method that does that:

C#
    private void picMainImage_MouseMove(object sender, MouseEventArgs e) {
        if (_FileList == null) {
            return;
        }

        Point mousePos = picMainImage.PointToClient(MousePosition);
        if ((string)btnThumbnail.Tag == "Thumbnail") {
            // Handle the MouseMove event to display a tooltip
            int clickedIndex = MouseEventFileIndex(mousePos);
            if (_FileList.Count > 0 && clickedIndex < _FileList.Count && clickedIndex >= 0) {
                toolTip.SetToolTip(picMainImage, Path.GetFileName(_FileList[clickedIndex]));
            }
        }
...

In testing with a large number of images I found that thumbnails towards the end of the thumbnails image were not being recognized. It turned out that the X and Y positions accessed via MouseEventArgs e are declared as int (32-bit) but are limited to <short>short values (16-bit). I was concerned that I would have to page the thumbnails if the vertical dimension exceeded 32,767 pixels. I asked ChatGPT about this issue. Here is the conversation.

Me

MouseEventArgs e.X and e.Y are defined as ints but act like 16-bit integers. When e.y exceeds 32767 it takes a negative value, such as -32749. Is there a way around that?

ChatGPT

Yes, you're correct. The e.X and e.Y properties in the MouseEventArgs class are indeed of type int. However, they are limited by the underlying Win32 API, which uses 16-bit signed integers for mouse coordinates. To handle large coordinates properly, you can use the PointToClient method of the control to convert screen coordinates to client coordinates. Here's how you can do it:

C#
private void YourControl_MouseMove(object sender, MouseEventArgs e)
{
    // Convert screen coordinates to client coordinates
    Point clientPoint = YourControl.PointToClient(MousePosition);

    // Now you can use clientPoint.X and clientPoint.Y which are of type int
}

By using PointToClient, you can ensure that your mouse coordinates are correctly interpreted regardless of the screen resolution or the position of your control within the form.

Me

Where is MousePosition defined/declared?

ChatGPT

MousePosition is a static property of the Control class in C#. It represents the current position of the mouse pointer in screen coordinates. You can access it from any context where you have access to the Control class, such as within a form or a control. Here's how you can access MousePosition:

C#
Point mousePosition = Control.MousePosition;

This retrieves the current mouse position as a Point object with X and Y coordinates representing the position of the mouse pointer on the screen.

Displaying EXIF information

Photographers are always interested in the exposure triangle of a photo. This information is contained in the EXIF (Exchangeable image file format) tags and values embedded in the image. There is a lot of other useful information contained in the EXIF tags, such as camera make and model, lens specifications, and more. Choose Images lets you configure which EXIF tags you want to see and the order you prefer. Different camera makers provide different sets of EXIF tags inside the images produced by their cameras. If you shoot with different cameras, you need a way to select the brand. In Choose Images you do this by specifying the RAW extension in the Settings menu. Then, when you choose the EXIF tags you want to see, you can associate them with a particular RAW extension. If you switch to a different camera brand, you can change the RAW extension and create a new set of EXIF tags for that extension. Some software strips EXIF tags from JPG images so this information is not always available. However, photos taken directly from camera media will always have EXIF information. Choose Images uses the MetadataExtractor library to extract EXIF tags and values. This is the code used to do that.

C#
private Dictionary<string, string> LoadAllEXIFTagsFromJPGImage(FileStream fs) {

    Image jpgImage = null;
    Dictionary<string, string> d = new Dictionary<string, string>();

    // Reset the file position to the beginning
    fs.Seek(0, SeekOrigin.Begin);

    // Load the image from the FileStream
    jpgImage = Image.FromStream(fs);

    using (MemoryStream stream = new MemoryStream()) {
        jpgImage.Save(stream, System.Drawing.Imaging.ImageFormat.Jpeg);

        // Rewind the stream to the beginning
        stream.Seek(0, SeekOrigin.Begin);

        // Extract EXIF information from the MemoryStream
        var directories = ImageMetadataReader.ReadMetadata(stream);
        foreach (var directory in directories) {
            foreach (var tag in directory.Tags) {
                if (d.ContainsKey(tag.Name)) {
                    continue;
                }
                d.Add(tag.Name, tag.Description);
            }
        }
    }
    return d;
}

Note that some EXIF tags are duplicates when the directories are ignored, so Choose Images only uses the first one found. The Settings menu includes the option Choose EXIF Tags to display for this RAW extension. When this is chosen, the following dialog is displayed. It allows the user to select which EXIF options to be displayed. The selected Tags can be rearranged into your preferred order using the right-hand side list box. You can use the mouse to highlight a tag and drag it into position or use the key board up and down arrows to highlight a tag and the same keys holding the ctrl key down to move the selection into position.

Screen Shot of dialog used to select EXIF tags to display

The code to manage the ordering of EXIF tags is based on code generated by ChatGPT that lets the user rearrange the EXIF tags shown in a ListBox named lstOrdered. My contribution was to require the user to press the Ctrl key while moving items using the arrow keys. Otherwise, you'd have to use the mouse to move to the tag you wanted to move.'

C#
private void lstOrdered_MouseDown(object sender, MouseEventArgs e) {
    _dragIndex = lstOrdered.IndexFromPoint(e.X, e.Y);
    if (_dragIndex != ListBox.NoMatches) {
        _dragging = true;
    }
}
private void lstOrdered_MouseMove(object sender, MouseEventArgs e) {
    if (_dragging && e.Button == MouseButtons.Left) {
        lstOrdered.DoDragDrop(lstOrdered.Items[_dragIndex], DragDropEffects.Move);
    }
}
private void lstOrdered_DragOver(object sender, DragEventArgs e) {
    Point point = lstOrdered.PointToClient(new Point(e.X, e.Y));
    int hoverIndex = lstOrdered.IndexFromPoint(point);

    if (hoverIndex != ListBox.NoMatches && hoverIndex != _dragIndex) {
        lstOrdered.SelectedIndex = hoverIndex;
    }
    e.Effect = DragDropEffects.Move;
}
private void lstOrdered_DragDrop(object sender, DragEventArgs e) {
    Point point = lstOrdered.PointToClient(new Point(e.X, e.Y));
    _dropIndex = lstOrdered.IndexFromPoint(point);

    if (_dropIndex != ListBox.NoMatches && _dropIndex != _dragIndex) {
        object dragItem = lstOrdered.Items[_dragIndex];
        lstOrdered.Items.RemoveAt(_dragIndex);
        lstOrdered.Items.Insert(_dropIndex, dragItem);
    }
    _dragging = false;
    lstOrdered.SelectedIndex = _dropIndex;
}
private void lstOrdered_KeyDown(object sender, KeyEventArgs e) {
    int index = lstOrdered.SelectedIndex;
    if (index < 0)
        return;

    // Check if Ctrl key is pressed
    bool ctrlPressed = (Control.ModifierKeys & Keys.Control) == Keys.Control;

    if (e.KeyCode == Keys.Up && index > 0) {
        if (ctrlPressed) {
            MoveItem(-1);
            e.Handled = true;
        }
    }
    else if (e.KeyCode == Keys.Down && index < lstOrdered.Items.Count - 1) {
        if (ctrlPressed) {
            MoveItem(1);
            e.Handled = true;
        }
    }
    else if (e.KeyCode == Keys.Enter) {
        ConfirmMove();
        e.Handled = true;
    }
}
private void MoveItem(int direction) {
    // Calculate new index using move direction
    int newIndex = lstOrdered.SelectedIndex + direction;

    // Ensure new index is within bounds
    if (newIndex < 0 || newIndex >= lstOrdered.Items.Count)
        return;

    // Swap items
    object selected = lstOrdered.SelectedItem;
    lstOrdered.Items.Remove(selected);
    lstOrdered.Items.Insert(newIndex, selected);

    // Select the moved item
    lstOrdered.SetSelected(newIndex, true);
}
private void ConfirmMove() {
    _dragIndex = lstOrdered.SelectedIndex;
}

You can view the EXIF data for an image by selecting the Information button on the tool bar. You can close it by clicking the button again or by clicking the Close at the bottom of the display. If you don't close the EXIF display, you can still navigate to other images using the tool bar arrow keys and the EXIF information will be updated as you do so.

Screen Shot of EXIF tags being displayed

If you have set a RAW extension then Choose Images will use the EXIF tags you associated with that RAW extension. Since the EXIF tags in the JPG previews files were copied from the original RAW image, you can ensure the EXIF tags that you select are appropriate.

Opening Destination Folders

If you shoot and process RAW then you will want to copy RAW image files from your camera media to a folder on your PC that you use for staging RAW photos. If you only shoot JPG, then you will want to copy your JPG files to a folder on your PC that you use for JPG images. There are File Menu options for opening one or the other. If you open both but the source folder contains RAW files, then only the RAW files are copied.

Resizing Image Files

Choose Images needs to resize images when it creates thumbnails from a file of full-sized JPG images (source files are JPG images) or medium to large JPG files (source files are preview JPG files extracted from RAW files). It also needs to resize images when the user changes the zoom or when the user resizes the Choose Images window when the zoom is set to Fit to Window. Luckily, from a quality point of view, these resize operations never require images to be upsized from the originals. Thumbnails are created by resizing large JPGs down to much smaller images. The single line of code to do this is found in the method to create composite images shown under Displaying Thumbnails above. The following method is used when zooming or when the form is resized.

C#
private void UpdateZoomedImage() {

    if (pnlMainFrame.Width <= 0) {
        return;
    }

    Size size;
    switch (_ZoomFactor) {
        case 0.0f:
            int width = Math.Min(pnlMainFrame.Width - 4, _OriginalImage.Photo.Width);
            int height = (int)((float)width / _ImageAspectRatio);
            if (height > pnlMainFrame.Height) {
                height = pnlMainFrame.Height;
                width = (int)((float)height * _ImageAspectRatio);
            }
            size = new Size(width - 5, height - 5);
            break;
        default:
            size = new Size((int)(_OriginalImage.Photo.Width * _ZoomFactor), (int)(_OriginalImage.Photo.Height * _ZoomFactor));
            break;
    }
    if (size.Width < 80) {
        return;
    }
    picMainImage.Size = new Size(pnlMainFrame.Width - 5, pnlMainFrame.Height - 5);
    Bitmap zoomedBitmap = null;
    try {
        // Create a new bitmap to draw the zoomed image
        zoomedBitmap = new Bitmap((int)(size.Width), (int)(size.Height));

        // Create a graphics object from the bitmap
        using (Graphics g = Graphics.FromImage(zoomedBitmap)) {
            // Set the interpolation mode to high quality
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
            g.CompositingMode = CompositingMode.SourceCopy;
            g.CompositingQuality = CompositingQuality.HighQuality;
            g.SmoothingMode = SmoothingMode.HighQuality;
            g.PixelOffsetMode = PixelOffsetMode.HighQuality;

            // Draw the image onto the bitmap
            g.DrawImage(_OriginalImage, new Rectangle(0, 0, zoomedBitmap.Width, zoomedBitmap.Height));
        }
        // Set the zoomed bitmap as the image of the PictureBox
        _DisplayedImage = zoomedBitmap;
        picMainImage.Image = _DisplayedImage;

        // Irrelevant code omitted

    }
    catch (Exception ex) {
        MessageBox.Show(_Messages[2] + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        zoomedBitmap.Dispose();
        return;
    }
    RotateImage(_Rotation);
}
private void RotateImage(RotateFlipType rotation) {
    picMainImage.Image = RotateBitMap((Bitmap)picMainImage.Image, rotation);
}

I used a more sophisticated resizing method when zooming in on a large image file as opposed to down-sizing an image file to thumbnail size.

Logging

Choose Images can log information to a log file. It creates a new log file each time you start the program. The information it logs includes:

  1. Duplicate file name {DSC9999.XXX} skipped. Can't have duplicate names in destination folder (Error)
  2. Show Image read {DSC9999.XXX} ({99} Mbytes)
  3. Show Image took {999}msecs
  4. Get EXIF Tags took {999}msecs
  5. ExifTool - extracting preview images from RAW files in {FolderName} took {9.99} secs
  6. {999} JPG preview files extracted from RAW files
  7. {999} {RAW} thumbnails loaded.
  8. {999} MB read from media
  9. Memory Usage (GB) {99.99)
  10. Copy Image failed for {PathName} (Error)
  11. {999} {extension}" thumbnails load aborted"
  12. Copying new RAW files from {SourceFolderName} to Temporary folder" took {9.99} secs

Items in braces are replaced by actual values in the log file. Logging is controlled from the Logging menu item which lets you enable or disable logging, view logfiles and delete log files. Viewing simply displays the log files in Notepad or whichever text editor you have configured for txt extensions.

Using ChatGPT as a coding assistant

As a retired software developer, I am not working full-time and staying up-to-date with the latest and greatest technology. But I do code as a hobbyist and I use search engines when I want to figure out how to do something. Yes, you can get the answers you need but it takes time to sort out the most appropriate solution and adapt it to your requirements.

I started asking ChapGTP questions about how to achieve certain tasks. One of my first questions was about panning an Image in a Picture Box. ChatGPT created a working C# example that served as an initial model. I went through a number of iterations trying to get panning to work perfectly. By expressing my questions using my variable and object names, I could get ChatGPT to create code I could use immediately.

Sometimes, I could get answers to problems that would have bugged me for hours. A few examples stand out. I couldn't for the life of me get the DataGrid to display data the way I wanted. ChatGPT's answers weren't working either. I sent ChatGPT this question, including code:

Here's where my problem is:

C#
// Styling the header columns
DataGridViewCellStyle headerStyle = new DataGridViewCellStyle();
headerStyle.BackColor = Color.DarkBlue;
headerStyle.ForeColor = Color.White;
headerStyle.Font = new Font("Tahoma", 13, FontStyle.Bold, GraphicsUnit.Pixel);
dataGridView1.ColumnHeadersDefaultCellStyle = headerStyle;

The only style that works is the font. The colors get ignored. Why?

ChatGPT came up with the answer.

I apologize for the confusion. The issue might be related to the visual style applied to the DataGridView, and the default visual style may override some of the specified styles. To enforce the specified styles, you can try setting the EnableHeadersVisualStyles property of the DataGridView to false. This ensures that the DataGridView doesn't use the visual styles of the operating system for the header cells.

Here's another example of where ChatGPT found a solution for me. I had a flickering issue at the end of a panning operation that used the mouse. ChatGPT suggested updating the panned image on a timer rather than in reaction to every mouse tremor. I had another flickering issue when I decided to show thumbnails as they were being loaded into a composite image inside a Picture Box. ChatGPT told me how to use Double Buffering.

ChatGPT's response

Enable double buffering on your Panel and PictureBox controls to reduce flickering.

C#
public ChooseImages()
{
    InitializeComponent();

    // Enable double buffering for the panel and picture box
    pnlMainFrame.DoubleBuffered(true);
    picMainImage.DoubleBuffered(true);
}

public static class ControlExtensions
{
    public static void DoubleBuffered(this Control control, bool enable)
    {
        var doubleBufferPropertyInfo = control.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        doubleBufferPropertyInfo.SetValue(control, enable, null);
    }
}

ChatGPT doesn't always give you correct or complete answer in response to your questions, but you can engage it in conversation and work towards a solution. For example, it took a number of exchanges to figure out why my DataGrid heading settings weren't being accepted. I look at ChatGPT as a smart coding assistance that knows a lot about C#.

Conclusions

Choose Images is designed to be the front-end to a photographer's workflow. Its primary mission is to make it easy to transfer images you want to work with from your camera media and skip those that are not needed or are not acceptable. It is most useful when you have a lot of photographs to process. They might be from a sporting event, a birding excursion, a wildlife adventure, a vacation or some other situation where you take lots of photographs. It replaces an earlier version that had much less flexibility and functionality.

If you want to write programs to work with RAW files, then ExifTool is invaluable. It allows Choose Images to extract preview images and EXIF information from virtually any RAW file you might encounter. Moreover, it helps you process your photos faster than working with default JPG files delivered by your camera. This is because most manufacturers embed multiple preview JPGS of different sizes in their RAW images. Reading and displaying 1616px x 1080px JPG images is much faster than using 8000px x 6000px images.

Developing Choose Images required me to learn about manipulating image files using C#. The need to display images that the user could zoom and pan helped me develop techniques using standard .Net controls without having to delve too deep into low level image processing. These techniques can be adapted by programmers needing to work with images and image files. The trick that simplified panning and scrolling was to place a PictureBox inside a panel with its Autoscroll propertty set to true. You could then manipulate the image by setting the Autoscroll properties, as required, remembering to negate the co-ordinates when you reset the Autoscroll position.

The development process exposed some underlying weaknesses in Windows. The fact that mouse co-ordinates are limited to 16-bit integers was a surprise. Monitors for photo editing already offer resolutions as high as 6144x3456px. The images from my camera are 9568x6376px. So, one can foresee images that exceed 32,767 pixels in either dimension. I ran into this issue when I created a composite image of thumbnails that was over 32,767 pixels high. ChatGPT showed me the work-around, but it is just that - a work-around. I also found it very easy to run out of memory. It wasn't due to memory leaks; it was just that Windows had trouble handling too many large images at once. It was the move to a camera that produces 60MB RAW images that exposed these issues. One would think that a 64-bit architecture with 64GB of RAM would not run out of memory, but it does.

I found ChatGPT to be a valuable coding assistant. It was far easier to find ways to do things by asking ChatGPT instead of using search engines. ChatGPT has been trained on huge amounts of code and is able to produce solutions with uncanny speed. It is not always right and it can be bamboozled. It obviously had limited knowledge of ExifTool and I gave up trying to use ChatGPT to build a wrapper for it. I got a lot of help and useful code working with Chat GTP.

The source code is freely available. If you find bugs or make enhancements you'd like to share, let me know.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)