LDC #92: Capturing Images from the Clipboard

Occasionally I have had the need to serially capture images for later use. There are a number of utilities available, some free, with varying capabilities, that do this. Recently I was asked to try get download a bunch of photos from a site that had no simple way to download images. To accomplish this, I wrote a little script that lets a user capture images, presumably from a screenshot or from a right-click “Copy” command, and serially store them.

A picture next to a clipboard with the same image

Within Legato there is a function that directs Windows to add the current dialog page to the clipboard chain. The clipboard chain is a series of windows that can receive a notification regarding the clipboard contents changing. Each window in the chain is notified anytime the content of the clipboard is altered. The
ClipboardAttachDialog
function both attaches and releases a dialog page:
int = ClipboardAttachDialog ( [boolean release] );

If the release parameter is not supplied, the dialog page is attached. If the parameter is supplied as TRUE, then the attach is discontinued. When a dialog page closes, the attachment is automatically removed.

Once attached, the dialog procedure “clipboard_change” is called every time the clipboard changes.

The Capture Script

Our sample script is very simple. It sets up the dialog and the clipboard and waits. The location where the captured image data is stored is preprogrammed as ‘Legato Image Capture’ on the user desktop.

                                                                // Monitor Dialog Resource
#beginresource

#define SC_CHANGES              101
#define SC_COUNT                102
#define SC_STATUS               103

CaptureDlg DIALOGEX 0, 0, 140, 44
EXSTYLE WS_EX_DLGMODALFRAME
STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Image Capture"
FONT 8, "MS Shell Dlg"
{
CONTROL "Clipboard Changes:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 6, 80, 8, 0
CONTROL "(none)", SC_CHANGES, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 80, 6, 30, 8, 0
CONTROL "Saved Images:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 18, 80, 8, 0
CONTROL "(none)", SC_COUNT, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 80, 18, 30, 8, 0
CONTROL "", SC_STATUS, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 12, 30, 120, 8, 0
}

#endresource

                                                                // Global Data
    string              fnBase;
    string              fnName;

    int                 cc;
    int                 ic;


int main() {

    fnBase = GetDesktopFolder() + "Legato Image Capture\\";
    fnName = "Image %03d.jpg";
    CreateFolders(fnBase);

    cc = -1;

    DialogBox("CaptureDlg", "ic_");
    
    return ERROR_NONE;
    }
    
    
int ic_load() {                                                 // Load Controls/Initialize
    ClipboardAttachDialog();
    return ERROR_NONE;
    }
    
int ic_clipboard_change() {                                     // Clipboard Notification
    handle              hDIB, hImage;
    string              s1;
    int                 rc;

    cc++;
    if (cc == 0) { return ERROR_NONE; }
    EditSetText(SC_CHANGES, cc);

    if (ClipboardIsDIBAvailable() == FALSE) { return ERROR_NONE; }

    ic++;
    EditSetText(SC_COUNT, ic);

    hDIB = ClipboardGetDIB();
    if (hDIB == NULL_HANDLE) { 
      EditSetText(SC_STATUS, "Error %08X ClipboardGetDIB", GetLastError());
      return ERROR_NONE;
      }

    hImage = ImageLoad(hDIB);
    if (hImage == NULL_HANDLE) { 
      EditSetText(SC_STATUS, "Error %08X ImageLoad", GetLastError());
      return ERROR_NONE;
      }
    
    s1 = fnBase + FormatString(fnName, ic);
    rc = ImageExportJPEG(hImage, s1);
    if (IsError(rc)) { 
      EditSetText(SC_STATUS, "Error %08X ImageExportJPEG", rc);
      return ERROR_NONE;
      }

    return ERROR_NONE;
    }

The first part of the script is a little dialog that has three display fields: the number of clipboard changes; the number of images saved; and a field for error messages. Note that the count fields are preloaded with ‘(none)’ so that, when first started, the dialog display as follows:
Image Capture Dialog

As changes are processed, the dialog changes:
Image Capture Dialog with captured images

We will come back to the dialog in a moment. There are four global variables defined:

    string              fnBase;
    string              fnName;

    int                 cc;
    int                 ic;

The base refers the path which is set to a folder in the user desktop. The name is also a base of sorts; since the images are numbered, it needs to have a formatted number value such as ‘%03d’ to place the image count. Note that this script will create the folder and overwrite the contents on each run. You can add logic to protect against overwrites, if desired.

The cc and ic track the ‘clipboard changes’ and the ‘image count’. You will later note that the cc is initialize to -1 because the clipboard procedure is called as the dialog is attached to the chain. So the chain procedure accommodates this to avoid a first false hit.

The main function sets up the path, the name, sets and initial change count and then launches the dialog. The load function of the dialog simply performs the clipboard attach using the ClipboardAttachDialog function.

Primary processing lies in the ic_clipboard_change function. This part counts the clipboard changes and displays the result in the SC_CHANGES static text control. If an image format has not been posted to the clipboard, the function exits:
if (ClipboardIsDIBAvailable() == FALSE) { return ERROR_NONE; }
After which the image count is updated. The function then gets the image as a DIB (Device Independent Bitmap). This works for many image sources but not all. Since I was interested in capturing screenshots, it works in this case. To expand this, one would have to look at retrieving other formats such as PNG, JPEG or GIF and then selecting which to store.

The DIB is loaded into an Image Object. This gives us option to export the data in a number of formats. Since our filename has a .jpg extension, the images are exported as JPEGs.

Finally, upon exit of the function, all the handles will be released and the code ready for the next clipboard change.

Conclusion
When the script is operating, it is ended by pressing the dialog close box [X] on the caption. DIB is widely used in Windows so the script works on a lot of programs including Word. I first used this on screen shots. It also seems to work on Firefox, Chrome and Edge to right-click and copy images from webpages. One noticeable deficiency is that images that have transparent backgrounds will lose their transparency because DIB does not carry the transparent bit. In such images, the transparent areas turn black.

Well, this is a simple capture program. It can be easily expanded to provide for user entry of path and base filename, skipping existing files and event adding a ‘Pause’ button to the dialog. See what you can do to make a better widget.
 

Scott Theis is the President of Novaworks and the principal developer of the Legato scripting language. He has extensive expertise with EDGAR, HTML, XBRL, and other programming languages.