[Windows] Test_ExifTool to test commands

Share your advanced PureBasic knowledge/code with the community.
Axolotl
Enthusiast
Enthusiast
Posts: 435
Joined: Wed Dec 31, 2008 3:36 pm

[Windows] Test_ExifTool to test commands

Post by Axolotl »

Hi folks!

A topic in the german forum made me curious about the tool ExifTool by Phil Harvey.
Here is a small (unfinished) program, with which you can test the ExifTool with several commands, if you want to.
As always with me my stuff is windows only, but the ExifTool is platform independent. (the usage of the commands are a little different on windows)

Currently I have entered only read accesses to image files in the command combo box. Further commands can be commented out or added by yourself. However, I did not test the commented commands, but took them from other forums and adapted them to the required windows 'syntax'.
But I strongly recommend to do the first attempts on copies and not on the original files.

I was trying to put all my coding guidelines in consideration here. Maybe someone likes the style. If not, is also not bad.

An update of this example code is not planned.
New challenges are already waiting for me ...

Happy Coding and stay healthy.

Code: Select all

;/=====================================================================================================================
;| File      : Test_ExifTool.pb
;| 
;| Purpose   : Make a simple Use of the ExifTool by Phil Harvey 
;|
;| Target OS : Windows (tested on windows only) 
;|
;| Created   : 2022-10-28 
;|
;| License   : Free, unrestricted, no warranty whatsoever - Use at your own risk 
;|
;|             WARNING! THE CODE IS PROVIDED "AS IS" with NO GUARANTEES OF ANY KIND! 
;|             USE THIS AT YOUR OWN RISK - YOU ARE THE ONLY PERSON RESPONSIBLE for 
;|             ANY DAMAGE THIS CODE MAY CAUSE - YOU HAVE BEEN WARNED! 
;|
;| >> 
;| >>  ExifTool by Phil Harvey 
;| >> 
;| >>  Websites: 
;| >>    Main Pages    : https://exiftool.org/ 
;| >>                  : http://exiftool.sourceforge.net/  .. an alternate ExifTool homepage 
;| >>    Programming   : https://exiftool.org/cpp_exiftool/ 
;| >>    Documentation : https://exiftool.org/exiftool_pod.html 
;| >>                  :         
;| >>                  :         
;| >> License: 
;| >>   This is free software; you can redistribute it and/or modify it under the same terms as Perl itself. 
;| >> 
;| >>   Perl-License   : https://dev.perl.org/licenses/ 
;| >> 
;|
;\=====================================================================================================================

EnableExplicit 
DebugLevel 9  ; in #PB_Compiler_Debugger Mode 


; ---== Check on general things, like OS and Compiler ==---------------------------------------------------------------

  CompilerIf #PB_Compiler_OS <> #PB_OS_Windows   ; ?? this is windows only code 
    CompilerWarning "The OS isn't supported, sorry." 
  CompilerEndIf 

  CompilerIf #PB_Compiler_Debugger   ; ?? only enable assert in debug mode 
    CompilerIf #PB_Compiler_Backend = #PB_Backend_Asm    ; <==> check on Compiler Backend == ASM 
      Debug "HINT: PB_Compiler_Backend == PB_Backend_Asm " 
    CompilerElseIf #PB_Compiler_Backend = #PB_Backend_C  ; <==> check on Compiler Backend == C 
      Debug "HINT: PB_Compiler_Backend == PB_Backend_C " 
    CompilerElse                                         ; <==> check on Compiler Backend == Unknown 
      Debug "HINT: PB_Compiler_Backend == Unknown ???? " 
    CompilerEndIf 
  CompilerEndIf 

  CompilerIf #PB_Compiler_Version > 600      ; <==> check on Compiler Version 
    CompilerWarning "WARNING: NEW CompilerVersion, check on return values (CreateXxxx(), StartDrawing(), etc. !" 
    ; Example of how you can check on result values ... 
    ; Define hImage = CreateImage(1, 32, 32, 24) 
    ; Debug Hex(hImage) 
    ; Debug Hex(ImageID(1)) 
    ; Debug "" 
  CompilerEndIf 


; - 
; ---== Application Specific Program Constants ==----------------------------------------------------------------------

  #ProgramName$        = "Test_ExifTool" 
  #ProgramVersion$     = "0." + #PB_Editor_BuildCount + "." + #PB_Editor_CompileCount 
  #ProgramAuthor$      = "AHa"
  #ProgramType$        = "Freeware"                            ; NONEware, Bearware, Pizzaware, etc. 

; ---== Define Program Standard Constants (same in all applications) ==------------------------------------------------

  ;>> build YEAR on purebasic compiler date (used in copyright) 
  #ProgramCompiled     = 1970 + (#PB_Compiler_Date / 31536000) ; easy calc on year (good enough for constant) $3C14DC 
  #ProgramCopyright$   = "Copyright (C) " + #ProgramCompiled + " by " + #ProgramAuthor$ 

CompilerIf #PB_Compiler_Debugger 
  #ProgramRelease$     = "Freeware (in Development)" 
CompilerElse 
  #ProgramRelease$     = "Freeware" 
CompilerEndIf 

  #Caption$            = #ProgramName$ + " " 
  #MainCaption$        = #Caption$ + #ProgramVersion$ + " " + #ProgramRelease$ 

  #ES$                 = "."           ; Extention Separator, syntax like #PS$ for Path Separator  
  #PreferencesExt$     = "prefs"       ; better than windows like ini, because format and behavior is different 
  #ExecutableExt$      = "exe"         ; use constants 


; ---== Enumeration ==-------------------------------------------------------------------------------------------------

Enumeration EWindow 
  #WINDOW_Main 
EndEnumeration

Enumeration EGadget
  #GADGET_StrDir 
  #GADGET_CbbCommand 
  #GADGET_BtnExecute 
  #GADGET_EdtOutput
EndEnumeration

Enumeration EEvent #PB_Event_FirstCustomValue 
  #EVENT_ThreadFinished 
EndEnumeration

Enumeration EStatusbar ; and StatusBarFields 
  #STATUSBAR                   ; Statusbar Number is Zero 
  #STATUSBAR_Author              = 0  ; first Field is Zero 
  #STATUSBAR_lblState          ; = 1 
  #STATUSBAR_currState         ; = 2  Idle, Running, Waiting, Finished, ?? 
  #STATUSBAR_FreeText          ; = 3  free 
  #STATUSBAR_lblDuration       ; = 4  Duration 
  #STATUSBAR_currDuration      ; = 5  execution time in ms  
EndEnumeration


; ---== User Defined Datatype Definition ==----------------------------------------------------------------------------

Structure TExifToolParameter 
  Command$
  WorkDir$             ; ?? 
  
  Executable$          ; the full path and filename of the exiftool.exe 
  WatchDogTime.i       ; check on working tool 
  ; 
  Thread.i             ; ?? available when Global is used instead of Threaded 
  AbortNow.i           ; ??   -"- 
EndStructure 


; ---== Application Specific Constants Definition ==-------------------------------------------------------------------

  #ExifTool_Name$      = "exiftool"    ; this is the name of the tool 
  #CmdSep$             = "#"           ; separate the command from the comment in combobox 


; ---------------------------------------------------------------------------------------------------------------------
; --- Global Variable 

Global ExifTool_Parameter.TExifToolParameter      ; Global, to work with member variables inside Thread 


; ---== Init with Defaults ==------------------------------------------------------------------------------------------

With ExifTool_Parameter
  \AbortNow      = #False   ; 
  \Thread        = 0        ; 
  \WatchDogTime  = 5000     ; ms 
  \WorkDir$      = GetCurrentDirectory()  

CompilerIf #PB_Compiler_Debugger  ; only on development time, avoid the requester during development  
; \Executable$ = "\path\to\exiftool.exe" 
; \WorkDir$    = "\path\to\pictures_and_images" 
CompilerEndIf 
EndWith 


; ---== Declare Procedures ==------------------------------------------------------------------------------------------

Declare Output(Message$) 
Declare SetStatus(Field, Status$) 
; Preferences 
Declare PreferenceChanged(State = #PB_Ignore)  ; return state of change 
Declare ReadPreferenceProgram()
Declare WritePreferenceProgram() 
; Window System 
Declare.l GetSystemMetrics(Index)  ; we need .l here instead of .i (windows api return value) 


; ---------------------------------------------------------------------------------------------------------------------

Procedure InitializeExifToolExecutable() 
  Protected file$ 

  file$ = ExifTool_Parameter\Executable$  ; use shorter local variable 
 ;If file$ = "" 
  If FileSize(file$) <= 0  ; exiftool.exe not available 
    file$ = #ExifTool_Name$ + #ES$ + #ExecutableExt$ ; set to default name (find the directory by requester) 
    file$ = OpenFileRequester("Please choose file to load", file$, "Executables (*.exe)|*.exe", 0) 
  EndIf 
  ExifTool_Parameter\Executable$ = file$                                       :Debug #PB_Compiler_Procedure + "  -->  '" + file$ + "'", 9 
  ProcedureReturn Bool(file$ <> "") 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure DownloadExifTool()  ; not implemented .. under construction .. could by nice to have this feature 
  Protected url$ 

;  url$ = "https://exiftool.org/" 

  ; result = ReceiveHTTPFile(url$, zip_filename$) 


; On Website:  https://exiftool.org/ 
;
; Find 
;   <blockquote><table class='dl lg'><tr><td><b>
;   <a name="alone">Windows Executable:</a>
;   <a href="exiftool-12.49.zip">
;          exiftool-12.49.zip</a> (6.5 MB)</b></td></tr></table></blockquote>
; 

EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure ExifTool_ExecuteInThread(*Parameter.TExifToolParameter)  
  Protected ProgID, cntErr, Exitcode, time, starttime, wdt, killPrg            :Debug #PB_Compiler_Procedure + "()" 
  Protected exec$, cmd$, dir$, workdir$, out$, err$, tmp$  

  time      = ElapsedMilliseconds()     ; watchdog started  
  starttime = time                      ; calc the entire execution time at the end 

  With *Parameter   ; set to local variables 
    exec$     = \Executable$            ; :Debug "Exec    = " + exec$ 
    cmd$      = \Command$               ; :Debug "Cmd     = " + cmd$ 
    workdir$  = \WorkDir$               ; :Debug "workdir = " + workdir$ 
    wdt       = \WatchDogTime           ; :Debug "wdt     = " + wdt 
  EndWith 

  Output("Start -- with command: " + cmd$) 
  Output("") 

  ProgID = RunProgram(exec$, cmd$, workdir$, #PB_Program_Open | #PB_Program_Read | #PB_Program_Write | #PB_Program_Error | #PB_Program_Hide) 
  If ProgID <> 0 
    SetStatus(#STATUSBAR_currState, "Running") 

    While ProgramRunning(ProgID) 
      If AvailableProgramOutput(ProgID)                ; collect stdout data 
        out$ = ReadProgramString(ProgID) 
        Output(out$) 
        time = ElapsedMilliseconds()                   ; watchdog updated 
      EndIf 

      err$ = ReadProgramError(ProgID) 
      While err$ <> ""                                 ; collect stderr data 
        cntErr + 1                                     ; count the error lines 
        Output(err$) 
        err$ = ReadProgramError(ProgID)  
      Wend 
      If err$ : time = ElapsedMilliseconds() : EndIf   ; watchdog updated 

      If ElapsedMilliseconds() - time > wdt            ; no output within watchdog time 
        Output("FATAL Error: Watchdog occured -> kill " + #ExifTool_Name$) 
        SetStatus(#STATUSBAR_currState, "TimeOut") 
        killPrg = #True 
        Break ; leave the loop 
      EndIf 

      If *Parameter\AbortNow = #True                   ; user wants to close the application 
        Output("User wants to close the application -> kill " + #ExifTool_Name$) 
        SetStatus(#STATUSBAR_currState, "Abort") 
        killPrg = #True 
        Break ; leave the loop 
      EndIf 
    Wend ; ProgramRunning()  

    Output("") 
    Output("Summary: ") 

    If killPrg = #True         ; watchdog timeout or user want stop 
      KillProgram(ProgID)      ; -> kill the exif tool right now 
    EndIf 

    Exitcode = ProgramExitCode(ProgID)   ; 
    Output("  ExitCode = " + Str(Exitcode)) 

    Select Exitcode 
      Case 0   ; on success 
        Output("  " + #ExifTool_Name$ + " exits with success") 
      Case 1   ; if an error occurred, 
        Output("  " + #ExifTool_Name$ + " exits with an error occurred") 
      Case 2   ; if all files failed 
        Output("  " + #ExifTool_Name$ + " exits with all files failed") 
    EndSelect 

    CloseProgram(ProgID) 

    tmp$ = Str(ElapsedMilliseconds() - starttime) 
   ;Output("  Duration = " + tmp$ + " ms.") 
    SetStatus(#STATUSBAR_currDuration, tmp$ + " ms.") 

  Else ; ProgID = 0 
    Output("FATAL Error: Cannot start '" + #ExifTool_Name$ + #ES$ + #ExecutableExt$ + "'!")  
  EndIf
 ;Output("Done.") 
  SetStatus(#STATUSBAR_currState, "Done") 
  PostEvent(#EVENT_ThreadFinished) 

  ProcedureReturn  ; end thread now 
EndProcedure 

;-
;----== GUI ==---------------------------------------------------------------------------------------------------------

Procedure OnEventSizeMainWindow() 
  Protected ww, wh 

  ww = WindowWidth(#WINDOW_Main)
  wh = WindowHeight(#WINDOW_Main) - StatusBarHeight(#STATUSBAR)  ; shrink the client area 

  ResizeGadget(#GADGET_StrDir, #PB_Ignore, #PB_Ignore, ww-16, #PB_Ignore) 
  ResizeGadget(#GADGET_CbbCommand, #PB_Ignore, #PB_Ignore, ww-16, #PB_Ignore) 
  ResizeGadget(#GADGET_BtnExecute, ww-96-8, #PB_Ignore, #PB_Ignore, #PB_Ignore) 
  ResizeGadget(#GADGET_EdtOutput, #PB_Ignore, #PB_Ignore, ww-16, wh-144) 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure OpenMainWindow(WndW=800, WndH=600)
  Protected hwnd, hfont                                                :Debug #PB_Compiler_Procedure 

  hwnd = OpenWindow(#WINDOW_Main, 0, 0, WndW, WndH, #MainCaption$, #PB_Window_SystemMenu|#PB_Window_ScreenCentered|#PB_Window_SizeGadget) 
  If hwnd <> 0 
    WindowBounds(#WINDOW_Main, WndW, WndH, #PB_Ignore, #PB_Ignore) 
    If CreateStatusBar(#STATUSBAR, hwnd)  ; still working in PB 6.00 LTS 
      AddStatusBarField(32)            ; = 0:  Author :) 

      AddStatusBarField(40)            ; = 1:  Status 
      AddStatusBarField(80)            ; = 2:  Idle, Running, Waiting, Finished, ?? 

      AddStatusBarField(#PB_Ignore)    ; = 3:  free 
      AddStatusBarField(56)            ; = 4:  Duration 
      AddStatusBarField(80)            ; = 5:  execution time in ms  
    EndIf 

    WndH - StatusBarHeight(#STATUSBAR)  ; shrinkk the client area 
   ;SetStatus(#STATUSBAR_Author, #ProgramAuthor$) 
    StatusBarText(#STATUSBAR, #STATUSBAR_Author, #ProgramAuthor$, #PB_StatusBar_BorderLess | #PB_StatusBar_Center) 
    SetStatus(#STATUSBAR_lblState, "Status:") 
    SetStatus(#STATUSBAR_lblDuration, "Duration:") 

    hfont = LoadFont(0, "Consolas", 8)  ; monospace font 

    TextGadget(#PB_Any, 8, 8, 400, 16, "ExifTool Working Directory (replace {DIR} in Command):")  
    StringGadget(#GADGET_StrDir, 8, 24, 784, 24, "")  ; replaces {DIR} in commands with e.g. "C:\Temp" 

    TextGadget(#PB_Any, 8, 56, 144, 16, "ExifTool Command:") 
    ComboBoxGadget(#GADGET_CbbCommand, 8, 72, 784, 24, #PB_ComboBox_Editable) 
    SetGadgetFont(#GADGET_CbbCommand, hfont)   ; set to monospace font 

    SHAutoComplete_(GadgetID(#GADGET_StrDir), $50000021 ) ;; #SHACF_AUTOAPPEND_FORCE_ON | #SHACF_AUTOSUGGEST_FORCE_ON | #SHACF_FILESYSTEM) 
    SendMessage_(GadgetID(#GADGET_StrDir), #EM_SETCUEBANNER, #Null, @"directory where exiftool should work with the files") 

    ButtonGadget(#GADGET_BtnExecute, 696, 104, 96, 24, "Execute") 

    TextGadget(#PB_Any, 8, 120, 144, 16, "Data from ExifTool:") 

    EditorGadget(#GADGET_EdtOutput, 8, 136, 784, 456-23)  ; StatusbarHeight() 
    SetGadgetFont(#GADGET_EdtOutput, hfont)   ; set to monospace font 

    BindEvent(#PB_Event_SizeWindow, @OnEventSizeMainWindow(), #WINDOW_Main) 
  EndIf 
  ProcedureReturn hwnd 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Macro AddComboboxItem(Command, Comment) 
  AddGadgetItem(#GADGET_CbbCommand, -1, LSet(Command, 40) + "  " + #CmdSep$ + " " + Comment) 
EndMacro 

Procedure FillWithRecentlyUsedCommands() 

  AddComboboxItem("-ver", #ExifTool_Name$ + " Version (only)") 
  AddComboboxItem("-ver -v2 ", "Additional Perl Version")  
  AddComboboxItem("--help ", "All Help Information ")  
  AddComboboxItem("-list_dir -T -ls-l -api systemtags -fast5 {DIR} ", "dir *.* ")  

  AddComboboxItem("-f -createdate -modifydate {DIR}", "Force printing of the requested tags anyway")  
  AddComboboxItem("-common {DIR}", "Print common meta information for all images in {DIR}") 
  AddComboboxItem("-a -u -g1 {DIR}", "Print all meta information for all images in {DIR}, sorted by group (for family 1)") 

;; Not tested commands 
;   AddComboboxItem("-datetimeoriginal="+ #DQUOTE$ + "2015:01:01 12:00:00"+ #DQUOTE$ + " {DIR}", "First set a base timestamp To all images") 
;   AddComboboxItem("" + #DQUOTE$ + "-datetimeoriginal+<0:0:${filesequence;$_*=3}" + #DQUOTE$ + " {DIR}", "Assign incremental timestamps  (3 seconds between each)")  
;   AddComboboxItem("-globalTimeShift -1 -time:all {DIR}", "Return all date/times, shifted back by 1 hour") 
;   AddComboboxItem("" + #DQUOTE$ + "-filename<createdate" + #DQUOTE$ + "-globaltimeshift " + #DQUOTE$ + "-0:0:1 0:0:0" + #DQUOTE$ + " -d %Y%m%d-%H%M%S.%%e {DIR}", "set the file name from the shifted CreateDate (-1 day) for all images in a directory")  
;   AddComboboxItem("-alldates+=1 -if " + #DQUOTE$ + "$CreateDate ge '2022:04:02'" + #DQUOTE$ + " {DIR}", "Add one hour To all images created on Or after Apr. 2, 2022") 


; 
; # Use the original date from the meta information to set the same
; #  file's filesystem modification date for all images in a directory.
; #  (Note that "-TagsFromFile @" is assumed if no other -TagsFromFile
; #  is specified when redirecting information as in this example.)
; exiftool "-FileModifyDate<DateTimeOriginal" DIR 
; 
; # Add 3 hours to the CreateDate and ModifyDate timestamps of two images.
; exiftool -createdate+=3 -modifydate+=3 a.jpg b.jpg


; "-DateTimeOriginal+=5:10:2 10:48:0" DIR

; exiftool -AllDates-=1 DIR

; exiftool -FileModifyDate-=1
; 13:24 29.10.2022 

  SetGadgetText(#GADGET_CbbCommand, GetGadgetItemText(#GADGET_CbbCommand, 0)) 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure Output(Message$) 
  AddGadgetItem(#GADGET_EdtOutput, -1, Message$) 
  SendMessage_(GadgetID(#GADGET_EdtOutput), #EM_SCROLLCARET, 0, 0)          ; Scroll to the End 
EndProcedure  

; ---------------------------------------------------------------------------------------------------------------------

Procedure SetStatus(Field, Status$) 
  StatusBarText(#STATUSBAR, Field, Status$, #PB_StatusBar_BorderLess) 
EndProcedure  

; ---------------------------------------------------------------------------------------------------------------------

Procedure ExifTool_StartExecution()  
  Protected cmd$, dir$                                                 :Debug #PB_Compiler_Procedure + "()" 

  ClearGadgetItems(#GADGET_EdtOutput) 

  cmd$ = GetGadgetText(#GADGET_CbbCommand)                             :Debug "  received command = '" + cmd$ + "'", 9  
  If cmd$ 
    cmd$ = StringField(cmd$, 1, #CmdSep$)  ; take valid command up to comment sign 
    cmd$ = RTrim(cmd$)                     ; remove trailing spaces 

    dir$ = GetGadgetText(#GADGET_StrDir) 
    If FileSize(dir$) = -2                                             :Debug "  {DIR} = '" + dir$ + "' found and set ", 9  
      cmd$ = ReplaceString(cmd$, "{DIR}", dir$)  ; exchange the place holder {DIR} 
    Else 
      cmd$ = ReplaceString(cmd$, "{DIR}", ".")   ; exchange the place holder {DIR} with current directory ?? 
    EndIf 

;   cmd$ = RemoveString(cmd$, "-k")  ; we do not want to pause the exiftool, because we cannot deal with it :)  

    With ExifTool_Parameter
      \AbortNow = #False 
      \Command$ = cmd$                                           :Debug "  Execution with Command = '" + cmd$ + "'" 
      \WorkDir$ = dir$ 
      \Thread   = CreateThread(@ExifTool_ExecuteInThread(), @ExifTool_Parameter) 
    EndWith 

  EndIf 
EndProcedure 


;----== main ==--------------------------------------------------------------------------------------------------------

Procedure main() 

  CoInitialize_(#Null)  ; windows API function: needed for the cuebanner stuff 

  If OpenMainWindow() 
    FillWithRecentlyUsedCommands() 
    ReadPreferenceProgram()  

    SetGadgetText(#GADGET_StrDir, ExifTool_Parameter\WorkDir$)  ; set to default 

    If Not InitializeExifToolExecutable() 
      Output("FATAL Error: ") 
      Output("  This application needs a valid " + #ExifTool_Name$ + ".exe to run properly.") 
      Output("  You can check out the already defined commands, but you are not able to run any of them.") 
      Output("  Please start the application again and chooce a correct " + #ExifTool_Name$ + ". ") 
      DisableGadget(#GADGET_BtnExecute, 1) ; disable button forever :) 
      SetStatus(#STATUSBAR_FreeText, "Error: " + #ExifTool_Name$ + " not selected.") 
    EndIf 
    Output("Using: " + ExifTool_Parameter\Executable$ + "") 
    SetStatus(#STATUSBAR_FreeText, "Using: " + GetFilePart(ExifTool_Parameter\Executable$) + "") 

    Repeat ; start main loop 
      Select WaitWindowEvent(4000)  ; after 4 s with no messages send the _None Event to clear the statusbar fields 
        Case #PB_Event_None         ; only if the thread is stopped 
          If Not IsThread(ExifTool_Parameter\Thread) 
            SetStatus(#STATUSBAR_currState, "Idle")    ; waiting for next command, etc. 
            SetStatus(#STATUSBAR_currDuration, "")     ; info of duration time is not longer needed 
          EndIf 
          
        Case #PB_Event_CloseWindow  ; only if we can stop the thread 
          If IsThread(ExifTool_Parameter\Thread) 
            If MessageRequester(#Caption$, #ExifTool_Name$ + "is still running. " + #LF + "Are you sure to close the application?", #MB_ICONQUESTION | #MB_YESNO) = #IDYES 
              ExifTool_Parameter\AbortNow = #True 
              
              If WaitThread(ExifTool_Parameter\Thread, 2000) = 0 ; time out occured 
                ; thread is still running ?? 
                ; let the program run. 
              Else ; no timeout means thread ended 
                Break  ; leave the main loop 
              EndIf 
            EndIf 
          Else 
            Break  ; leave the main loop 
          EndIf 

        Case #PB_Event_SizeWindow  
          ; resizing of the gadgets is done by OnEventSizeMainwindow() 
          PreferenceChanged(#True)  ; mark preferences changed 

        Case #EVENT_ThreadFinished                             ;:Debug "  #EVENT_ThreadFinished ", 9 
          DisableGadget(#GADGET_CbbCommand, 0) 
          DisableGadget(#GADGET_BtnExecute, 0) 

        Case #PB_Event_Gadget 
          Select EventGadget() 
            Case #GADGET_BtnExecute 
              DisableGadget(#GADGET_CbbCommand, 1) 
              DisableGadget(#GADGET_BtnExecute, 1) 
              ExifTool_StartExecution() 
          EndSelect 

      EndSelect 
    ForEver 

    WritePreferenceProgram() 
  EndIf 

  CoUninitialize_()  ; companion to CoInitialize_() 

  ProcedureReturn 0 
EndProcedure 


; ---== The main() ==--------------------------------------------------------------------------------------------------

main()  ; .. and this is where the whole mess begins 


;-
;----== Read /Write Preference Program Procedures ==-------------------------------------------------------------------

Procedure.s ProgramPreferences() 
  Static s_PrefsFile$ = "" 
  Protected file$, path$  

  If s_PrefsFile$ = ""  ; first call .. examine the preference filename only once 
    CompilerIf #PB_Compiler_Debugger         ; .. at development time 
      CompilerIf #PB_Compiler_Filename = "PB_EditorOutput.pb" 
        Debug "HINT: " + #PB_Compiler_Procedure + "() with unsaved main file " 
        file$ = ""  ; return empty string 
  	  CompilerElse 
  	    path$ = #PB_Compiler_FilePath 
  	    file$ = GetFilePart(#PB_Compiler_Filename, #PB_FileSystem_NoExtension) 
  	  CompilerEndIf 
    CompilerElse                             ; .. at runtime 
      file$ = ProgramFilename() 
      path$ = GetPathPart(file$) 
      file$ = GetFilePart(file$, #PB_FileSystem_NoExtension) 
    CompilerEndIf 
    If file$ 
      s_PrefsFile$ = path$ + file$ + #ES$ + #PreferencesExt$  ; full filename i.e. <MyAppName.prefs> 
    EndIf                                                            :Debug #PB_Compiler_Procedure+"() -> return '" + s_PrefsFile$ + "'" 
  EndIf 
  ProcedureReturn s_PrefsFile$ 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------
; -- <Number of Monitors> _ <entire width> x <entire height>  i.e. '1_1920x1080' 
Macro PreferenceDesktopInfoAsKey()  
  "Desktop" + Str(GetSystemMetrics(#SM_CMONITORS)) + "_" + Str(GetSystemMetrics_(#SM_CXVIRTUALSCREEN)) + "x" + Str(GetSystemMetrics_(#SM_CYVIRTUALSCREEN)) 
EndMacro 

; ---------------------------------------------------------------------------------------------------------------------

Procedure PreferenceChanged(State = #PB_Ignore)  ; return state of change 
  Static s_Changed = #False 

  If State = #False Or State = #True 
    s_Changed = State  ; set to new state 
  EndIf 
  ProcedureReturn s_Changed 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure ReadPreferenceMainWindow()  
  Protected value$, tmp$, x, y, w, h 

  value$ = ReadPreferenceString(PreferenceDesktopInfoAsKey(), "") 
  If value$ 
    tmp$ = StringField(value$, 1, ",")  : If tmp$ : x = Val(tmp$) : Else : x = #PB_Ignore : EndIf 
    tmp$ = StringField(value$, 2, ",")  : If tmp$ : y = Val(tmp$) : Else : y = #PB_Ignore : EndIf 
    tmp$ = StringField(value$, 3, ",")  : If tmp$ : w = Val(tmp$) : Else : w = #PB_Ignore : EndIf 
    tmp$ = StringField(value$, 4, ",")  : If tmp$ : h = Val(tmp$) : Else : h = #PB_Ignore : EndIf 
    ResizeWindow(#WINDOW_Main, x, y, w, h) 
  EndIf 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure WritePreferenceMainWindow()  
  Protected value$ 

  value$ = Str(WindowX(#WINDOW_Main)) + "," + Str(WindowY(#WINDOW_Main)) + "," + Str(WindowWidth(#WINDOW_Main)) + "," + Str(WindowHeight(#WINDOW_Main)) 
  WritePreferenceString(PreferenceDesktopInfoAsKey(), value$) 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure ReadPreferenceProgram()  ; .. under construction 
  Protected result, file$ 

  file$ = ProgramPreferences() 
  If file$ 
    result = OpenPreferences(file$, #PB_Preference_NoSpace | #PB_Preference_GroupSeparator) 
    If result = 0 And MessageRequester(#Caption$, "Create Preferences File?" + #LF$ + "File: '" + file$ + "'", #MB_ICONINFORMATION | #MB_YESNO) = #IDYES 
      result = CreatePreferences(file$, #PB_Preference_NoSpace | #PB_Preference_GroupSeparator) 
      If result <> 0 
        PreferenceComment(#ProgramName$ + " - preferences file - " + #ProgramCopyright$ + " - portable version! ") 
;       Debug "SET FILEDATE = " + SetFileDate(file$, #PB_Date_Created, #PB_Compiler_Date)    ; use this date for preferences ... 
      EndIf 
    EndIf 
  EndIf 

; OpenPreferences(ProgramPreferences(), #PB_Preference_NoSpace)
    PreferenceGroup("Desktop") 
      ReadPreferenceMainWindow() 

    PreferenceGroup("Common") 
      With ExifTool_Parameter
        \Executable$  = ReadPreferenceString("ExifToolExec",  \Executable$) 
        \WorkDir$     = ReadPreferenceString("WorkDir",       \WorkDir$) 
        \WatchDogTime = ReadPreferenceInteger("WatchDogTime", \WatchDogTime) 
      EndWith 

  ; PreferenceGroup("CommandList") 
  ; .. not implemented 

  ClosePreferences() 
EndProcedure 

; ---------------------------------------------------------------------------------------------------------------------

Procedure WritePreferenceProgram()   ; .. under construction 
  If PreferenceChanged() 
    If OpenPreferences(ProgramPreferences(), #PB_Preference_NoSpace)
      PreferenceGroup("Desktop") 
        WritePreferenceMainWindow() 

      PreferenceGroup("Common") 
        With ExifTool_Parameter 
          WritePreferenceString("ExifToolExec",  \Executable$) 
          WritePreferenceString("WorkDir",       \WorkDir$) 
          WritePreferenceInteger("WatchDogTime", \WatchDogTime) 
        EndWith 

    ; RemovePreferenceGroup("CommandList")  
    ; PreferenceGroup("CommandList") 
    ; .. not implemented 

      ClosePreferences() 
    EndIf
  EndIf
EndProcedure 

;----== Window System Functions ==-------------------------------------------------------------------------------------

Procedure.l GetSystemMetrics(Index)         ; .l is important (works in 64-bit) .. variable can be a .i type 
  ProcedureReturn GetSystemMetrics_(Index) 
EndProcedure 

;----== Bottom of File ==----------------------------------------------------------------------------------------------
Mostly running PureBasic <latest stable version and current alpha/beta> (x64) on Windows 11 Home