This is the first in (hopefully) a series of posts about vbScript. Please see my post vbScript - The Basics for more details on vbScript.

My wife and I take a lot of pictures. Naturally, we end up sending pictures to friends through email. I find it is unnecessary, and often inconsiderate to send full size images via email. When most of our friends end up viewing these images on a hand-held device, it is pointless to send an image wider than 600 pixels. As such, I have to repeatedly

  1. Copy the full size image to a temporary folder
  2. Run FastStone (my viewer of choice)
  3. Locate the image
  4. Resize it
  5. Save it

This gets tedious after a while. Fortunately, I found a free package, ImageMagick which allows me to do the resize from the command line. What I would like is to set up a special folder such that when I drop an image file into it, it gets automatically resized. I could easily do this by running a script in a loop and repeatedly polling a folder but this is wasteful in terms of resources. I would prefer to have Windows notify me when a new file appears in a folder. Fortunately Windows has the ability to create a FolderWatch that will sit idly by, consuming next to nothing in the way of resources, then do something when there is something to be done.

A couple of notes:

'#region Header 
'#endregion

are recognized by PrimalScript to define code folding (collapsible) regions and are treaded by the interpreter as comments.

Include "%INCLUDE%\iif.vbs"

This includes the code

Function iif ( ByVal condition , ByVal truevalue , ByVal falsevalue )
   If condition Then iif = truevalue: Else: iif = falsevalue: End If
End Function

to provide a ternary function that should have been in vbScript to begin with.

WatchFolder uses a Windows component, WMI (for Windows Management Instrumentation). WMI is complex enough that when I need to use it I just Google what I want then use it. I could spend months learning it but it's not worth my time to do that for the little I use it. Simply put, the query to create a FolderWatch is

query = "SELECT * FROM __InstanceOperationEvent WITHIN 2" & vbCrLf _
      & "  WHERE Targetinstance ISA 'CIM_DataFile'"       & VbCrLf _
      & "    AND TargetInstance.Drive='" & drive  & "'"   & VbCrLf _
      & "    AND TargetInstance.Path='"  & folder & "'"

It is not necessary to add vbCrLf (carriage return/line feed) except that it makes the query easier to read when displayed to the console (code is for humans to read). Once we execute the query we enter a loop which does nothing but wait for events which are then farmed out to external handlers. vbScript does not do callbacks so we simulate that functionality with the Execute command. If you have provided a handler to handle the Create event - for this example, let's assume you have created a function

Function Resize(file)

and you have copied myfile.jpg to the watched folder - what will result is the execution of the string (assuming you are watching the folder D:\temp\resize)

Resize("D:\temp\resize\myfile.jpg")

If you have not provided a handler for that event (passed a null string) then what results is

Execute ""

which does nothing. So now that we have a WatchFolder sub, how is it used? Here is the front end.

''#region Header                                                                        '
''                                                                                      '
''  Name:                                                                               '
''                                                                                      '
''      AutoResize.vbs                                                                  '
''                                                                                      '
''  Description:                                                                        '
''                                                                                      '
''      This script uses a Windows folder watch process to monitor a given folder for   '
''      File Create events. When a file is created, the Resize function is called by    '
''      the folder watch process. If the created file was of type jpg, the file is      '
''      read, resized acdording to the command line arguments, then saved back into     '
''      the same file.                                                                  '
''                                                                                      '
''  Notes:                                                                              '
''                                                                                      '
''      Requires the installation of ImageMagick (https://www.imagemagick.org/)         '
''                                                                                      '
''      I recommend running this with wscript.exe instead of cscript.exe                '
''                                                                                      '
''  Audit:                                                                              '
''                                                                                      '
''      2018-06-16  rj  Original code                                                   '
''                                                                                      '
''#endregion                                                                            '

Include "%INCLUDE%\WatchFolder.vbs"

Set wso = CreateObject("Wscript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")

''Get folder name

If WScript.Arguments.Unnamed.Count = 0 Then
    WScript.Echo "You must give a folder name on the command line"
    WScript.Quit 
End If

folder = WScript.Arguments.Unnamed(0)

If Not fso.FolderExists(folder) Then
    fso.CreateFolder(folder)
End If 

''Get command line options                                                              '

width = 600

For Each arg In WScript.Arguments.Named
    Select Case Lcase(arg)
        Case "w","width"    : width = Wscript.Arguments.Named(arg)
        Case "?","help"     : Help("") : Wscript.Quit
        Case Else           : Help(arg): Wscript.Quit
    End Select
Next

'check if width is numeric

If not IsNumeric(width) Then
    WScript.Echo "Width must be a numeric value"
    WScript.Quit 
End If

width = CInt(width)

'chbeck if width is in an acceptable range

If width < 100 Or width > 2000 Then
    WScript.Echo "Please enter a width value from 100-2000"
    WScript.Quit 
End If

'start the folder watch with a callback to Resize on 'Create' event

WatchFolder folder, "ReSize", "", ""

Function ReSize (file)

    If LCase(fso.GetExtensionName(file)) = "jpg" Then 
        cmd = "magick " & """" & file & """" & " -resize " & width & " """ & file & """"
        wso.Run cmd, 7, True                 '7 = run minimized
        wso.Popup file & " resized", 5, "AutoResize", 4096
    End If

End Function

''Display help info                                                                     '
Function Help (str)

    If Len(str) > 0 Then WScript.Echo VbCrLf & ">>>Unknown option: /" & str

    Wscript.Echo ""
    Wscript.Echo "AutoResize folder"
    WScript.Echo ""
    WScript.Echo "  Watch a folder for the creation of jpg files. If triggered, resize"
    WScript.Echo "  the file to a given width in the range 100-2000)."
    Wscript.Echo ""
    Wscript.Echo "    Description"
    Wscript.Echo ""
    Wscript.Echo "    Options:"
    Wscript.Echo ""
    WScript.Echo "        /width:###    (/w) width (default is 600 pixels)"
    Wscript.Echo "        /help         (/?) show this help"

End Function

''Include the given file in the global namespace                                        '
Function Include ( ByVal file )
    Dim wso: Set wso = CreateObject("Wscript.Shell")
    Dim fso: Set fso = CreateObject("Scripting.FileSystemObject")
    ExecuteGlobal(fso.OpenTextFile(wso.ExpandEnvironmentStrings(file)).ReadAll)
End Function

You'll see that almost all of the front end is housekeeping (checking arguments, providing help text). The interesting parts are

WatchFolder folder, "ReSize", "", ""

This line calls WatchFolder and passes it four parameters

  1. The folder to monitor
  2. The name of the handler for Create events (as a string)
  3. A null string for Delete events (ignore these events)
  4. A null string for Modufy events (ignore these events)

The other code of interest is the handler

Function ReSize (file)

    If LCase(fso.GetExtensionName(file)) = "jpg" Then 
        cmd = "magick " & """" & file & """" & " -resize " & width & " """ & file & """"
        wso.Run cmd, 7, True                 '7 = run minimized
        wso.Popup file & " resized", 5, "AutoResize", 4096
    End If

End Function

The actual command line that we will execute will look something like

magick "d:\temp\resize\myimage.jpg" -resize 600 "d:\temp\resize\myimage.jpg"

and to execute it we use the Run method of the Wscript.Shell object, wso. The Run method is passed three parameters

  1. The name of the program to run
  2. The Window type of the new process (7=run minimized)
  3. A boolean indicating whether to wait for completion (True) or not (False)

The next line calls the PopUp method which (in this case) will display a popup dialog that will display on top (4096). If the user doesn't close it within 5 seconds it will auto close.

In my shell:startup folder I have created a shortcut

wscript.exe d:\temp\resize

so that this process will always sit in the background ready to work when needed.

''#region Header                                                                        '
''                                                                                      '
''  Name:                                                                               '
''                                                                                      '
''      WatchFolder.vbs                                                                 '
''                                                                                      '
''  Description:                                                                        '
''                                                                                      '
''      Watch the given folder for file create, delete and modify events. You provide   '
''      the handler code by passing the name of the handler when you call this Sub.     '
''      Each handler must have the form:                                                '
''                                                                                      '
''          Function handlerName(filename)                                              '
''                                                                                      '
''      where handlerName is any valid function name and filename can be any variable   '
''      name. The handler will be passed the name of the file associated with the       '
''      given event. If you don't want to specify a handler for a particular event      '
''      then pass a null string for the handler. For example, to watch "D:\temp" For    '
''      the creation or deletion, but not modification of files you would Do            '
''                                                                                      '
''          WatchFolder "D:\temp","MyCreateHandler","MyDeleteHandler",""                '
''                                                                                      '
''      and you would be expected to provide                                            '
''                                                                                      '
''          Function MyCreateHandler (thefile)                                          '
''              'your custom code                                                       '
''              Wscript.Echo Now(),"File",thefile","was created"                        '
''          End Function                                                                '
''                                                                                      '
''          Function MyDeleteHandler (thefile)                                          '
''              'your custom code                                                       '
''              Wscript.Echo Now(),"File",thefile","was deleted"                        '
''          End Function                                                                '
''                                                                                      '
''  Note:                                                                               '
''                                                                                      '
''      The callback is implemented via the Execute statement which allows you to build '
''      vbScript code on the fly. If the Executed string is the null string then no     '
''      action occurs. Thus we build the string in two pieces                           '
''                                                                                      '
''          1) The name of the function (handler) to Call                               '
''          2) The rest of the command as ("D:\example.txt")                            '
''                                                                                      '
''      In the case where no handler is requested, part two is set to ""                '
''                                                                                      '
''      The only way to exit the event "loop" is to interrupt the running script.       '
''                                                                                      '
''  Audit:                                                                              '
''                                                                                      '
''      2018-03-02  rj  original code                                                   '
''                                                                                      '
''#endregion                                                                            '

Include "%INCLUDE%\iif.vbs"

Sub WatchFolder ( folder , Create_Handler, Delete_Handler, Modify_Handler )

    Dim wmi         'Windows Management Instrumentation object  '
    Dim query       'wmi query for selecting events             '
    Dim events      'Collection of events from wmi              '
    Dim e           'Single event from collection               '
    Dim drive       'Drive letter (including :) from folder     '
    Dim parms       'file name associated with the event        '

    'A folder name for the query must have \\ instead of \ in the path, and
    'must also end with \\

    drive  = Left(folder,2)
    folder = Mid(folder,3)
    If Right(folder,1) <> "\" Then folder = folder & "\"
    folder = Replace(folder,"\","\\")

    'Set a value for WITHIN to report events within the given number of seconds. The
    'vbCrLf chars are not needed for the query but they make the displayed query
    'easier for a human to read.

    query = "SELECT * FROM __InstanceOperationEvent WITHIN 2" & vbCrLf _
          & "  WHERE Targetinstance ISA 'CIM_DataFile'"       & VbCrLf _
          & "    AND TargetInstance.Drive='" & drive  & "'"   & VbCrLf _
          & "    AND TargetInstance.Path='"  & folder & "'"

    'If running from the command line, display the wmi query

    if instr(1,wscript.fullname,"cscript",vbTextCompare) > 0 then
        WScript.Echo VbCrLf & query & VbCrLf
    end if

    'Get Windows Management Intrumentation (WMI) object and run query

    Set wmi = GetObject( "winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2" )
    Set events = wmi.ExecNotificationQuery(query)

    'This loop will suspend at .NextEvent until events are reported for processing

    Do  'Press CTRL-C in console window to Stop

        Set e = events.NextEvent()
        parms = "(""" & e.TargetInstance.Name & """)"

        Select Case e.Path_.Class
            Case "__InstanceCreationEvent"
                Execute Create_Handler & iif(Create_Handler = "","",parms)
            Case "__InstanceDeletionEvent"
                Execute Delete_Handler & iif(Delete_Handler = "","",parms)
            Case "__InstanceModificationEvent"
                Execute Modify_Handler & iif(Modify_Handler = "","",parms)
        End Select

    Loop

End Sub