Sorting pictures can be a time consuming task. Many devices such as cell phones and cameras save pictures with sequential numbers. While this is great because we, as the users, don’t need to name our photos as we take them. However, it does mean that sorting the resulting photos can be time consuming. Luckily we can use Legato to create a basic sorting interface. Unlike many of the scripts in this blog this script is intentionally incomplete. It is a basic framework in which you can build your own image processing. You could edit the script to move all of the images into folders based on the specified names. This would sort the pictures into events or groups. You could also edit the script to run a program on the images with names. You could create thumbnails of named images. There are many possibilities including reworking the script to work with more than images.
Friday, November 09. 2018
LDC #110: Organizing Pictures
Let’s start by creating the resources. We want to have an interface where the user can select images and then edit the name for the image. This means we need a control to show a list. I chose a data control for this because there are a few columns that can be displayed for each image such as the ID, File name, and the chosen name. While you can do column using a list box the data control offers additional functionality and ease of use. We also need a text control for the name and a button the user can press to apply their changes. For added ease of use we can use an image preview control. This control is part of the GoFiler line of products and allows an image to be displayed by using EditSetText. Here is the completed dialog:
#beginresource #define DC_APPLY 101 #define DC_NAME 201 #define DC_ID 202 #define DC_LIST 301 #define DC_PICTURE 302 NameListDlg DIALOGEX 0, 0, 400, 180 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Organize Files" FONT 8, "MS Shell Dlg" { CONTROL "", DC_LIST, "data_control", LBS_NOTIFY | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | WS_VSCROLL | WS_HSCROLL, 10, 10, 258, 110, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 272, 4, 1, 148, 0 CONTROL "", DC_PICTURE, "img_preview_control", WS_CHILD | WS_VISIBLE | WS_BORDER, 276, 9, 116, 142, 0 CONTROL "ID:", DC_ID, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 11, 126, 253, 8, 0 CONTROL "Name:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 11, 140, 29, 8, 0 CONTROL "", DC_NAME, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 40, 138, 176, 12, 0 CONTROL "Apply", DC_APPLY, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 223, 137, 45, 14, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 157, 389, 1, 0 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 295, 161, 45, 14, 0 CONTROL "Close", IDCANCEL, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 347, 161, 45, 14, 0 } #endresource
Now that we have our dialog we can work on the main processing function. This script is designed to be opened and then run but if you wanted to use the function many times a menu hook would probably be better. For the main function we need to do a few things, load our settings, load the list of files, load our name list, check for files in the name list, and then we can present the dialog. There will be more processing after the dialog but this is a good place to start.
// Path to Process #define MY_PATH "C:\\Users\\david.theis\\Downloads\\Images\\Test\\" string files[][]; int f_cnt; void main() { handle hFF; string date; string csvfile; string file; qword modtime, lmodtime; string names[][]; int n_cnt; int nx, fx; int rc; // Load Settings date = GetSetting("Retained", "Last Date"); if (date != "") { lmodtime = StringToDate(date); } csvfile = GetSetting("Retained", "CSV File"); if (csvfile == "") { csvfile = AddPaths(MY_PATH, "names.csv"); }
We start out with a define for the directory that contains our pictures. This could be easily replaced with a browse dialog but for our example it’s good enough. Be sure to change the define if you run the script on your computer. Next we have two globals that make up the list of files. A string table for the properties of each file and an integer that is the size of the table. Now we can go into the main function. We start by loading the settings for the script. We are currently storing the last date we looked at the directory as well as the name of a CSV file that contains a mapping of IDs to names. This file doesn’t need to exist but by adding this we can map the same IDs to the same names the next time we run the script. We also convert the last modified date into the lmodtime qword variable for easy date comparisons. Because our script is using GetSetting it needs to be saved before it is run.
With that out of the way we can load the list of files.
// Load List f_cnt = 0; hFF = GetFirstFile(AddPaths(MY_PATH, "*.*")); if (IsError(hFF)) { MessageBox("No files."); return; } files[f_cnt]["ID"] = ""; files[f_cnt]["File"] = ""; files[f_cnt]["Name"] = ""; while (TRUE) { if (date != "") { modtime = GetFileModifiedTime(hFF); } else { modtime = lmodtime + 1; } if ((FindInString(GetName(hFF), ".png") < 0) && (FindInString(GetName(hFF), ".jpg") < 0)) { modtime = lmodtime - 1; } if (modtime > lmodtime) { file = ReplaceInStringRegex(GetName(hFF), "[^0-9]*([0-9]+)[^0-9]*\\..+", "$1"); if (file != "") { files[f_cnt]["Name"] = ""; files[f_cnt]["File"] = GetName(hFF); files[f_cnt]["ID"] = file; f_cnt++; } } rc = GetNextFile(hFF); if (IsError(rc)) { break; } } if (f_cnt == 0) { MessageBox("No files."); return; }
We use GetFirstFile on our path to get a list of all files in the path. The GetFirstFile function has been used in many blog posts but as a reminder it returns a handle. This handle in return can be used with many other functions to get information about the current file or move to the next file. We use this over the EnumerateFiles function because we want the modified time, alternatively we could use the EnumerateFolderDetails function but that returns more information than we need.
We then set up the columns in our files list. This is because in the dialog we will use the DataControlAddTable function so we want our columns to be in a predictable order. Now we can loop over the files, if we have a last run date we use the GetFileModifiedTime to get the modified time of the current file. If we don’t have a modified date we can set the modified time of the current file to lmodtime + 1. Basically, if we don’t have a last modified date we treat all files as new. We then check if the file is an image. If it isn’t we edit the modified time to be in the past thus skipping the file. Note that the script only checks for JPG and PNG files but others could be added.
If the modtime is greater than the lmodtime we actually process the file. So we use a regular expression to extract any numeric sequence from the file name. This only will extract the first numeric sequence in a name but the expression could be tailored to your specific needs. If we were able to get a numeric value from the name we add the file to our files list and increase f_cnt. At the end of the loop we use GetNextFile to get the next file. If there are no more files we break out of the loop.
With the files list created we can move on to loading the previous name list if any and applying it to our files list.
// Check Name list names = CSVReadTable(csvfile); n_cnt = ArrayGetAxisDepth(names); for (fx = 0; fx < f_cnt; fx++) { for (nx = 0; nx < n_cnt; nx++) { if (files[fx]["ID"] == names[nx][0]) { files[fx]["Name"] = names[nx][1]; break; } } }
We read the CSV file using the CSVReadTable function and then we go through all the files and see if their IDs are in our names table. If so we set the name for that file. Now we have all the information needed to present the dialog to the user.
// Present Dialog rc = DialogBox("NameListDlg", "nl_"); if (IsCancel(rc)) { return; }
The DialogBox function takes the name of the dialog and a prefix for our dialog functions. We will cover the “nl_” functions after we are done discussing the main function. If the user cancels the dialog we just leave since there is nothing to be done.
After the user is done with the dialog we can run our actual processing. Remember as stated above this script doesn’t actually do anything but I left some comments in the file to indicate where the processing could go.
// Any processing // We could move files into directories based on name // Or batch rename // Or run programs on ones with specific names
After the processing we need to save our settings and the list of names. Let’s start by editing our name table with any updated IDs.
// Update List for (fx = 0; fx < f_cnt; fx++) { for (nx = 0; nx < n_cnt; nx++) { if (files[fx]["ID"] == names[nx][0]) { names[nx][1] = files[fx]["Name"]; break; } } if (nx == n_cnt) { names[nx][0] = files[fx]["ID"]; names[nx][1] = files[fx]["Name"]; n_cnt++; } }
We iterate over each file and see if it’s ID is in the names list, if it is we update the name. If nx equals n_cnt after our loop ended it means we didn’t find the ID in the names list so we can add it to the list and increase n_cnt. Lastly, we save the table and our settings.
// Save List CSVWriteTable(names, csvfile); // Save Settings PutSetting("Retained", "Last Date", FormatDate(GetUTCTime())); PutSetting("Retained", "CSV File", csvfile); }
This completes the main function but as it stands our script will not work as we don’t have any of the dialog functions yet. So let’s work on those now. We need to do a load function so we can set up the list. We will need an action function to deal with the button and the selection changes. Finally, we will likely want a function to set the controls when the selection changes. Let’s take a look a the load function.
int nl_load() { DataControlSetColumnPositions(DC_LIST, 70, 170, 270); DataControlSetColumnHeadings(DC_LIST, "ID", "File", "Name"); DataControlAddTable(DC_LIST, files); DataControlSetRowSelection(DC_LIST, 0); nl_set_controls(); return ERROR_NONE; }
We use the DataControlSetColumnPositions and DataControlSetColumnHeadings functions to set the headings for our data control. We then use the DataControlAddTable with our files list to add the entire list to the control in a single function call. Since the main function exits if there are no files in the files list it is safe to select the first item in the data control using the DataControlSetRowSelection function. Since we changed the selection we run our set controls function. Let’s look at that function now.
void nl_set_controls() { int fx; fx = DataControlGetRowSelection(DC_LIST); if (fx >= 0) { EditSetText(DC_ID, "ID: " + files[fx]["ID"]); EditSetText(DC_NAME, files[fx]["Name"]); EditSetText(DC_PICTURE, AddPaths(MY_PATH, files[fx]["File"])); } }
We get the current selection in the data control using the DataControlGetRowSelection function. If the control has a selection (which it should) we use the index to set the text of our ID label, Name text field and the Picture control. We can use EditSetText function for all three of these control types which simplifies our code. Please note that because the EditSetText does not function asynchronously if the image file that is being loaded is large the dialog may change selections slowly. A solution to fix this issue may be to use DialogSetTimer to load the picture with a delay to let the selection change faster while the image is slightly delayed.
The last part we need to cover is the action function.
void nl_action(int id, int code) { string name; int fx; int rc; if (id == DC_LIST) { if (code == DCN_SELECT_CHANGE) { nl_set_controls(); } return; } if (id == DC_APPLY) { fx = DataControlGetRowSelection(DC_LIST); if (fx >= 0) { name = EditGetText(DC_NAME); files[fx]["Name"] = name; DataControlSetCellText(DC_LIST, fx, 2, name); } } }
If the id is the data control and the code is the selection change we run our set controls function. This causes our other controls to change when the selection changes. If the id is the Apply button we need to save the name and update the list. We start by getting the current selection. If there is a valid selection we get the name using the EditGetText function and then we set the name in the files list. This actually changes the data but we want the user to see that something happened so we use the DataControlSetCellText to change the name in the control as well.
This completes the entire script. We now have a script that loads all the images in a folder with numeric names and then presents a dialog where a user can enter information about the images and get a preview of the images to aid in naming the images. As it stands if you run the script on a folder from a digital camera it would remember the last time the script was run and not query for pictures that were there last time. Overall the basis for a useful utility to help identify images written in Legato. We all can use a little help sorting all the cat pictures on the Internet.
Here is a copy of the entire script:
#beginresource #define DC_APPLY 101 #define DC_NAME 201 #define DC_ID 202 #define DC_LIST 301 #define DC_PICTURE 302 NameListDlg DIALOGEX 0, 0, 400, 180 EXSTYLE WS_EX_DLGMODALFRAME STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Organize Files" FONT 8, "MS Shell Dlg" { CONTROL "", DC_LIST, "data_control", LBS_NOTIFY | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | WS_VSCROLL | WS_HSCROLL, 10, 10, 258, 110, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 272, 4, 1, 148, 0 CONTROL "", DC_PICTURE, "img_preview_control", WS_CHILD | WS_VISIBLE | WS_BORDER, 276, 9, 116, 142, 0 CONTROL "ID:", DC_ID, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 11, 126, 253, 8, 0 CONTROL "Name:", -1, "static", SS_LEFT | WS_CHILD | WS_VISIBLE, 11, 140, 29, 8, 0 CONTROL "", DC_NAME, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 40, 138, 176, 12, 0 CONTROL "Apply", DC_APPLY, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 223, 137, 45, 14, 0 CONTROL "", -1, "static", SS_ETCHEDFRAME | WS_CHILD | WS_VISIBLE, 6, 157, 389, 1, 0 CONTROL "OK", IDOK, "button", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 295, 161, 45, 14, 0 CONTROL "Close", IDCANCEL, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 347, 161, 45, 14, 0 } #endresource // Path to Process #define MY_PATH "C:\\Users\\david.theis\\Downloads\\Images\\Test\\" string files[][]; int f_cnt; void main() { handle hFF; string date; string csvfile; string file; qword modtime, lmodtime; string names[][]; int n_cnt; int nx, fx; int rc; // Load Settings date = GetSetting("Retained", "Last Date"); if (date != "") { lmodtime = StringToDate(date); } csvfile = GetSetting("Retained", "CSV File"); if (csvfile == "") { csvfile = AddPaths(MY_PATH, "names.csv"); } // Load List f_cnt = 0; hFF = GetFirstFile(AddPaths(MY_PATH, "*.*")); if (IsError(hFF)) { MessageBox("No files."); return; } files[f_cnt]["ID"] = ""; files[f_cnt]["File"] = ""; files[f_cnt]["Name"] = ""; while (TRUE) { if (date != "") { modtime = GetFileModifiedTime(hFF); } else { modtime = lmodtime + 1; } if ((FindInString(GetName(hFF), ".png") < 0) && (FindInString(GetName(hFF), ".jpg") < 0)) { modtime = lmodtime - 1; } if (modtime > lmodtime) { file = ReplaceInStringRegex(GetName(hFF), "[^0-9]*([0-9]+)[^0-9]*\\..+", "$1"); if (file != "") { files[f_cnt]["Name"] = ""; files[f_cnt]["File"] = GetName(hFF); files[f_cnt]["ID"] = file; f_cnt++; } } rc = GetNextFile(hFF); if (IsError(rc)) { break; } } if (f_cnt == 0) { MessageBox("No files."); return; } // Check Name list names = CSVReadTable(csvfile); n_cnt = ArrayGetAxisDepth(names); for (fx = 0; fx < f_cnt; fx++) { for (nx = 0; nx < n_cnt; nx++) { if (files[fx]["ID"] == names[nx][0]) { files[fx]["Name"] = names[nx][1]; break; } } } // Present Dialog rc = DialogBox("NameListDlg", "nl_"); if (IsCancel(rc)) { return; } // Any processing // We could move files into directories based on name // Or batch rename // Or run programs on ones with specific names // Update List for (fx = 0; fx < f_cnt; fx++) { for (nx = 0; nx < n_cnt; nx++) { if (files[fx]["ID"] == names[nx][0]) { names[nx][1] = files[fx]["Name"]; break; } } if (nx == n_cnt) { names[nx][0] = files[fx]["ID"]; names[nx][1] = files[fx]["Name"]; n_cnt++; } } // Save List CSVWriteTable(names, csvfile); // Save Settings PutSetting("Retained", "Last Date", FormatDate(GetUTCTime())); PutSetting("Retained", "CSV File", csvfile); } void nl_set_controls() { int fx; fx = DataControlGetRowSelection(DC_LIST); if (fx >= 0) { EditSetText(DC_ID, "ID: " + files[fx]["ID"]); EditSetText(DC_NAME, files[fx]["Name"]); EditSetText(DC_PICTURE, AddPaths(MY_PATH, files[fx]["File"])); } } int nl_load() { DataControlSetColumnPositions(DC_LIST, 70, 170, 270); DataControlSetColumnHeadings(DC_LIST, "ID", "File", "Name"); DataControlAddTable(DC_LIST, files); DataControlSetRowSelection(DC_LIST, 0); nl_set_controls(); return ERROR_NONE; } void nl_action(int id, int code) { string name; int fx; int rc; if (id == DC_LIST) { if (code == DCN_SELECT_CHANGE) { nl_set_controls(); } return; } if (id == DC_APPLY) { fx = DataControlGetRowSelection(DC_LIST); if (fx >= 0) { name = EditGetText(DC_NAME); files[fx]["Name"] = name; DataControlSetCellText(DC_LIST, fx, 2, name); } } }
David Theis has been developing software for Windows operating systems for over fifteen years. He has a Bachelor of Sciences in Computer Science from the Rochester Institute of Technology and co-founded Novaworks in 2006. He is the Vice President of Development and is one of the primary developers of GoFiler, a financial reporting software package designed to create and file EDGAR XML, HTML, and XBRL documents to the U.S. Securities and Exchange Commission. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato