Ga naar de hoofdnavigatie Ga naar de hoofdinhoud Ga naar de voettekst van de pagina
{ Picture Organizer (PicOrg) is a free open source Directory rename
  tool created by Theo Langstraat and licensed under GNU/GPL.

  @version
  1.0.0.0 (February 9 2023)
  1.1.0.0 (December 9 2023) Status form added

  @copyright
  Copyright (C) 2023 Theo Langstraat *) }

unit MainForm;

interface

uses
  DB,
  FireDAC.Comp.Client, FireDAC.Stan.Intf, FireDAC.Stan.Param,
  FireDAC.Stan.Error,
  FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf, FireDAC.Comp.DataSet,
  FireDAC.Stan.Option,
  System.Classes, System.Generics.Collections, System.IniFiles, System.IOUtils,
  System.JSON, System.JSON.Readers, System.JSON.Types, System.UITypes,
  System.StrUtils, System.SysUtils, System.Variants, System.Win.Registry,
  Vcl.ComCtrls, Vcl.Controls, Vcl.DBCtrls, Vcl.DBGrids, Vcl.Dialogs,
  Vcl.ExtCtrls,
  Vcl.ExtDlgs, Vcl.FileCtrl, Vcl.Forms, Vcl.Grids, Vcl.StdCtrls,
  Winapi.Messages, Winapi.ShlObj, Winapi.Windows, Vcl.OleCtrls, SHDocVw,
  StatusForm, Util, TL.Components, SettingsForm, Vcl.Buttons;

type
  TfrmMain = class(TForm)
    btnDestinationDir: TButton;
    btnExecute: TButton;
    btnSourceDir: TButton;
    btnStart: TButton;
    FileOpenDialog: TFileOpenDialog;
    gbSettings: TGroupBox;
    lblDestinationDir: TLabel;
    lblSourceDir: TLabel;
    lvError: TListView;
    OpenPictureDialog: TOpenPictureDialog;
    pnlBottom: TPanel;
    pnlButtons: TPanel;
    pnlDetails: TPanel;
    pnlError: TPanel;
    pnlLists: TPanel;
    pnlMain: TPanel;
    pnlTop: TPanel;
    StatusBar: TStatusBar;
    tblImageProperties: TFDMemTable;
    // Much faster then ListView in Report modus if more then 3000 items
    tblImagePropertiesSourceFile: TStringField; // tblImageProperties.Fields[0]
    tblImagePropertiesFileModifyDate: TStringField;
    // tblImageProperties.Fields[1]
    tblImagePropertiesFileCreateDate: TStringField;
    // tblImageProperties.Fields[2]
    tblImagePropertiesDateTimeOriginal: TStringField;
    // tblImageProperties.Fields[3]
    tblImagePropertiesOriginalDate: TStringField;
    // tblImageProperties.Fields[4]
    tblImagePropertiesOriginalYear: TStringField;
    // tblImageProperties.Fields[5]
    tblImagePropertiesDirectory: TStringField; // tblImageProperties.Fields[6]
    tblImagePropertiesDestinationFile: TStringField;
    // tblImageProperties.Fields[7]
    tblImagePropertiesDuplicate: TStringField; // tblImageProperties.Fields[8]

    lblSourceFile: TLabel;
    lblFileModifyDate: TLabel;
    lblFileCreateDate: TLabel;
    lblDateTimeOriginal: TLabel;
    lblOriginalDate: TLabel;
    lblOriginalYear: TLabel;
    lblDirectory: TLabel;
    lblDestinationFile: TLabel;
    lblDuplicate: TLabel;

    dbgImageProperties: TDBGrid;
    dbNavigator: TDBNavigator;
    dsImageProperties: TDataSource;

    dbtSourceFile: TDBText;
    dbtFileModifyDate: TDBText;
    dbtFileCreateDate: TDBText;
    dbtDateTimeOriginal: TDBText;
    dbtOriginalDate: TDBText;
    dbtOriginalYear: TDBText;
    dbtDirectory: TDBText;
    dbtDestinationFile: TDBText;
    dbtDuplicate: TDBText;
    rgFileAction: TRadioGroup;
    Button2: TButton;
    LDBGrid1: TLDBGrid;
    function GetFileType(FileName: string): TFileType;
    function OpenIniFileInstance: TCustomIniFile;
    procedure btnDestinationDirClick(Sender: TObject);
    procedure btnExecuteClick(Sender: TObject);
    procedure btnSourceDirClick(Sender: TObject);
    procedure btnStartClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure LoadConfig;
    procedure lvErrorResize(Sender: TObject);
    procedure SaveConfig;
    function Scan(Path: string): TFunctionResult;
    procedure rgFileActionClick(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    SourceDir: string;
    DestinationDir: string;

    FileCounter: integer;
    TotalFilesDetected: integer;

    State: TState;

    file_list: TStringlist;
    ImageExt, RawExt, WebExt, VideoExt, RemoveExt, OtherExt: string;
    ImageDir, RawDir, WebDir, VideoDir, RemoveDir, NoEXIFDir, OtherDir: string;
    duplicatesDir: string;
    Sr: TStringReader;
    Reader: TJsonTextReader; // Fastest JSON implementation
    function CountFilesInFolder(Path: string): integer;
    function CreateDestName(SourceFile, OriginalDate, OriginalYear,
      Directory: string): string;
    function GetWhatsAppDate(FileName: string): string;
    procedure CreateReader(Str: string);
    function GetExifData: TFunctionResult;
    procedure RestructureDirectory;
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;
  frmStatus: TfrmStatus;
  FileListName: string;

implementation

{$R *.dfm}

uses
  ExifTool;

procedure TfrmMain.CreateReader(Str: string);
begin
  if Reader <> nil then
    Reader.Free;
  if Sr <> nil then
    Sr.Free;
  Sr := TStringReader.Create(Str);
  Reader := TJsonTextReader.Create(Sr);
end;

function TfrmMain.OpenIniFileInstance: TCustomIniFile;
begin
  { HKEY_CURRENT_USER\Software\... }
  Result := TRegistryIniFile.Create('Software\Langstraat\' + Application.Title);
end;

function GetSpecialFolderPath(CSIDLFolder: integer): string;
var
  FilePath: array [0 .. MAX_PATH] of char;
begin
  SHGetFolderPath(0, CSIDLFolder, 0, 0, FilePath);
  Result := FilePath;
end;

procedure TfrmMain.SaveConfig;
var
  ConfigFile: TCustomIniFile;
begin
  ConfigFile := OpenIniFileInstance();
  with ConfigFile do
    try
      WriteString('general', 'Copyright', 'Theo Langstraat - 2024');
      WriteString('general', 'Version', '1.2');
      WriteString('general', 'Date', '7-5-2024');

      WriteString('locations', 'SourceDir', SourceDir);
      WriteString('locations', 'DestinationBaseDir', DestinationDir);
      WriteString('locations', 'ImageDir', ImageDir);
      WriteString('locations', 'RawDir', RawDir);
      WriteString('locations', 'WebDir', WebDir);
      WriteString('locations', 'VideoDir', VideoDir);
      WriteString('locations', 'RemoveDir', RemoveDir);
      WriteString('locations', 'NoEXIFDir', NoEXIFDir);
      WriteString('locations', 'OtherDir', OtherDir);
      WriteString('locations', 'DuplicatesDir', DuplicatesDir);

      WriteString('filetypes', 'image', ImageExt);
      WriteString('filetypes', 'raw', RawExt);
      WriteString('filetypes', 'web', WebExt);
      WriteString('filetypes', 'video', VideoExt);
      WriteString('filetypes', 'remove', RemoveExt);
      WriteString('filetypes', 'other', OtherExt);

      if WindowState <> wsMaximized then
      begin
        WriteInteger('frmMain', 'Top', frmMain.Top);
        WriteInteger('frmMain', 'Left', frmMain.Left);
        WriteInteger('frmMain', 'Height', frmMain.Height);
        WriteInteger('frmMain', 'Width', frmMain.Width);
      end;
      WriteBool('frmMain', 'WindowState', WindowState = wsMaximized);
      WriteDateTime('general', 'LastRun', Now);
      WriteInteger('fileaction', 'Action', rgFileAction.ItemIndex);
    finally
      Free;
    end;
end;

procedure TfrmMain.LoadConfig;
var
  ConfigFile: TCustomIniFile;
begin
  ConfigFile := OpenIniFileInstance();
  with ConfigFile do
    try
      SourceDir := ReadString('locations', 'SourceDir',
        GetSpecialFolderPath(CSIDL_MYPICTURES));
      DestinationDir := ReadString('locations', 'DestinationBaseDir',
        DestinationDir);

      ImageDir := ReadString('locations', 'ImageDir', '\01 - image');
      RawDir := ReadString('locations', 'RawDir', '\02 - raw');
      VideoDir := ReadString('locations', 'VideoDir', '\03 - video');
      WebDir := ReadString('locations', 'WebDir', '\04 - web');
      DuplicatesDir := ReadString('locations', 'DuplicatesDir', '\05 - duplicates');

      NoEXIFDir := ReadString('locations', 'NoEXIFDir', '\06 - noexif');
      OtherDir := ReadString('locations', 'OtherDir', '\07 - other');
      RemoveDir := ReadString('locations', 'RemoveDir', '\08 - remove');

      ImageExt := ReadString('filetypes', 'image', ImageExt).ToLower;
      RawExt := ReadString('filetypes', 'raw', RawExt).ToLower;
      WebExt := ReadString('filetypes', 'web', WebExt).ToLower;
      VideoExt := ReadString('filetypes', 'video', VideoExt).ToLower;
      RemoveExt := ReadString('filetypes', 'remove', RemoveExt).ToLower;
      OtherExt := ReadString('filetypes', 'other', OtherExt).ToLower;

      frmMain.Top := ReadInteger('frmMain', 'Top', frmMain.Top);
      frmMain.Left := ReadInteger('frmMain', 'Left', frmMain.Left);
      frmMain.Height := ReadInteger('frmMain', 'Height', frmMain.Height);
      frmMain.Width := ReadInteger('frmMain', 'Width', frmMain.Width);

      case ReadBool('frmMain', 'WindowState', WindowState = wsMaximized) of
        true:
          WindowState := wsMaximized;
        false:
          WindowState := wsNormal;
      end;

      rgFileAction.ItemIndex := ReadInteger('fileaction', 'Action', 1);

    finally
      Free;
    end;
end;

procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  SaveConfig;
  if frmStatus <> nil then
    frmStatus.Free;
  if file_list <> nil then
    file_list.Free;
end;

procedure TfrmMain.FormCreate(Sender: TObject);
var
  Col: TListColumn;
begin
  btnExecute.Enabled := false;
  lvError.Clear;
  lvError.Columns.Clear;
  Col := lvError.Columns.Add;
  Col.Caption := StrMessagesFromEXIFTo;
  Col.Alignment := taLeftJustify;
  Col.Width := frmMain.Width - 10;

  file_list := TStringlist.Create;
  frmStatus := TfrmStatus.Create(Self);

  { Default filters }
  ImageExt := '.jpeg .JPG .tif .tiff';
  RawExt := '.afphoto .CR2 .CR3 .CRW .dng .psd';
  WebExt := '.bmp .png';
  VideoExt := '.MOV .mp4';
  RemoveExt := '.info .ini .json .mxc3 .pp2 .pp3 .THM .url .xml .xmp';
  OtherExt := '.AAE .bib .BridgeSort .CTG .dat .docx .pdf';
  LoadConfig;

  TotalFilesDetected := CountFilesInFolder(SourceDir);
  lblSourceDir.Caption := 'Number of Files: ' + IntToStr(TotalFilesDetected) +
    ' - ' + SourceDir;
  lblDestinationDir.Caption := DestinationDir;
  rgFileActionClick(Sender);

  Self.Caption := 'Picture directory organizer ' +
    TL.Components.Folder.AppVersion + ' - © 2024 Theo Langstraat';
end;

procedure TfrmMain.FormResize(Sender: TObject);
var
  i, j: integer;
begin
  j := 0;
  for i := 0 to 8 do // Calculate total fixed width for first 0..8 panels
    j := j + StatusBar.Panels[i].Width;
  StatusBar.Panels[9].Width := StatusBar.Width - j;
  // Calculate remaining width for panel 9 comprising the File name in progress
end;

function TfrmMain.CountFilesInFolder(Path: string): integer;
var
  c: integer;

  procedure CountFiles(Path: string);
  const
    Success: integer = 0;
  var
    f: TSearchRec;
  begin
    if FindFirst(Path + '\*.*', faAnyFile, f) = Success then
    begin
      repeat // until FindNext(f) <> Success
        if (f.Attr and faDirectory) <> 0 then
        begin // is directory
          if (f.Name[1] <> '.') then // not current or parent directory
            CountFiles(Path + '\' + f.Name); // recurses all sub-directories
        end // f.attr = faDirectory
        else // // is not directory
          Inc(c);
      until FindNext(f) <> Success;
      System.SysUtils.FindClose(f);
    end; // FindFirst(Path + '\*.*', faDirectory, f) = Success
  end;

begin
  c := 0;
  CountFiles(Path);
  Result := c;
end;

procedure TfrmMain.btnSourceDirClick(Sender: TObject);
begin
  with FileOpenDialog do
  begin
    DefaultFolder := SourceDir;
    Options := [fdoPickFolders];
    if Execute then
    begin
      SourceDir := FileName;
      TotalFilesDetected := CountFilesInFolder(SourceDir);
      lblSourceDir.Caption := 'Number of Files: ' + IntToStr(TotalFilesDetected)
        + ' - ' + SourceDir;
    end;
  end;
  SaveConfig;
end;

procedure TfrmMain.btnDestinationDirClick(Sender: TObject);
begin
  with FileOpenDialog do
  begin
    DefaultFolder := DestinationDir;
    Options := [fdoPickFolders];
    if Execute then
    begin
      DestinationDir := FileName;
      lblDestinationDir.Caption := DestinationDir;
    end;
  end;
  SaveConfig;
end;

procedure TfrmMain.btnStartClick(Sender: TObject);
begin
  State := stInit;
  while State <> stCompleted do
  begin
    case State of // State Machine

      stInit:
        begin
          frmMain.Enabled := false;
          btnExecute.Enabled := false;
          { Selection of the desired EXIF fields are put in the first lines of the file_list.args file }
          file_list.Clear;
          file_list.Add('-SourceFile');
          file_list.Add('-FileModifyDate');
          file_list.Add('-FileCreateDate');
          file_list.Add('-DateTimeOriginal');
          FileCounter := 0;
          { clear all values in Statusbar.Panels }
          for var i := 0 to StatusBar.Panels.Count - 1 do
            if Odd(i) then // odd Panels are values, even Panels are fixed texts
              StatusBar.Panels[i].Text := '';
          { Clear MemTable }
          tblImageProperties.Active := false;
          tblImageProperties.Active := true;
          lvError.Clear;
          FileCounter := 0;
          frmStatus.FilesMoved := 0;
          frmStatus.DuplicateFiles := 0;
          frmStatus.FilesProcessed := FileCounter;
          frmStatus.FilesDetected := TotalFilesDetected;
          frmStatus.TotalFilesDetected := TotalFilesDetected;
          frmStatus.Cancel := false;
          frmStatus.btnOk.Enabled := false;
          frmStatus.btnCancel.Enabled := true;
          frmStatus.Caption := 'Scanning...';
          frmStatus.Show;
          State := stScan;
        end;

      stScan:
        begin
          frmStatus.State := State;

          case Scan(SourceDir) of // starting directory for Scan

            frOk:
              begin
                frmStatus.FilesProcessed := FileCounter;
                State := stGetExif
              end;

            frCancel:
              State := stCancel;

            frError:
              begin
                MessageDlg(SourceDir + StrNoFilesFoundIn, mtInformation,
                  [mbOk], 0, mbOk);
                State := stCancel;
              end;

          end;

        end;

      stGetExif:
        begin
          frmStatus.State := State;
          if file_list.Count > 4 then // something to process?
          begin
            Application.ProcessMessages;
            { EXIFTool works not well with ANSI charset therefore we use UTF8 file_list.args file }
            file_list.SaveToFile(FileListName, TEncoding.UTF8);
            GetExifData;
            btnExecute.Enabled := true;
          end
          else // ' not found or folder is empty.'
            MessageDlg(SourceDir + StrNoFilesFoundIn, mtInformation,
              [mbOk], 0, mbOk);
          if not frmStatus.Cancel then
            State := stCompleting
          else
            State := stCancel;
        end;

      stCompleting:
        begin
          State := stCompleted;
          frmStatus.State := State;
          frmStatus.ProcessingFile := '';
          frmStatus.btnOk.Enabled := true;
          frmStatus.btnCancel.Enabled := false;
          frmStatus.Hide;
          frmStatus.ShowModal;
          frmStatus.Hide;
          frmMain.Enabled := true;
          frmMain.SetFocus;
        end;

      stCancel:
        begin
          State := stCompleted;
          { clear all values in Statusbar.Panels }
          for var i := 0 to StatusBar.Panels.Count - 1 do
            if Odd(i) then // odd Panels are values, even Panels are fixed texts
              StatusBar.Panels[i].Text := '';
          frmStatus.State := State;
          frmStatus.btnOk.Enabled := true;
          frmStatus.Hide;
          frmMain.Enabled := true;
          frmMain.SetFocus;
        end;

      stCompleted:
        ;

    end; // State Machine
    Application.ProcessMessages;
  end;
  State := stNone;
end;

procedure TfrmMain.Button2Click(Sender: TObject);
// ImageExt, RawExt, WebExt, VideoExt, RemoveExt, OtherExt: string;
// ImageDir, RawDir, WebDir, VideoDir, RemoveDir, NoEXIFDir, OtherDir: string;

begin
  frmSettings.LabeledEdit1.Text := ImageExt;
  frmSettings.LabeledEdit2.Text := RawExt;
  frmSettings.LabeledEdit3.Text := VideoExt;
  frmSettings.LabeledEdit4.Text := WebExt;
  frmSettings.LabeledEdit5.Text := OtherExt;
  frmSettings.LabeledEdit6.Text := RemoveExt;

  frmSettings.LabeledEdit7.Text := ImageDir;
  frmSettings.LabeledEdit8.Text := RawDir;
  frmSettings.LabeledEdit9.Text := VideoDir;
  frmSettings.LabeledEdit10.Text := WebDir;
  frmSettings.LabeledEdit11.Text := OtherDir;
  frmSettings.LabeledEdit12.Text := RemoveDir;
  frmSettings.LabeledEdit13.Text := DuplicatesDir;


  if frmSettings.ShowModal = mrOk then
  begin
    ImageExt := frmSettings.LabeledEdit1.Text;
    RawExt := frmSettings.LabeledEdit2.Text;
    VideoExt := frmSettings.LabeledEdit3.Text;
    WebExt := frmSettings.LabeledEdit4.Text;
    OtherExt := frmSettings.LabeledEdit5.Text;
    RemoveExt := frmSettings.LabeledEdit6.Text;

    ImageDir := frmSettings.LabeledEdit7.Text;
    RawDir := frmSettings.LabeledEdit8.Text;
    VideoDir := frmSettings.LabeledEdit9.Text;
    WebDir := frmSettings.LabeledEdit10.Text;
    OtherDir := frmSettings.LabeledEdit11.Text;
    RemoveDir := frmSettings.LabeledEdit12.Text;
    DuplicatesDir := frmSettings.LabeledEdit13.Text;

    SaveConfig;
  end;

end;

procedure TfrmMain.btnExecuteClick(Sender: TObject);
begin
  frmMain.Enabled := false;
  frmStatus.Show;
  frmStatus.btnOk.Enabled := false;

  RestructureDirectory;
  btnExecute.Enabled := false;

  State := stCompleted;
  frmStatus.State := State;
  frmStatus.ProcessingFile := '';
  frmStatus.btnOk.Enabled := true;
  frmStatus.btnCancel.Enabled := false;
  frmStatus.Hide;
  frmStatus.ShowModal;
  frmStatus.Hide;
  frmMain.Enabled := true;
  frmMain.SetFocus;
end;

function TfrmMain.GetWhatsAppDate(FileName: string): string;
var
  Str: string;
  Splitted: TArray<String>;
  Prefix, ImageDate, ImageName: string;
begin
  { WhatsApp Filename Format: IMG-YYYYMMDD-WAXXXX.jpg
    Where YYYY is year, MM is month and DD is day.
    The WAXXXX just increments by one for every image taken on the same day, ex. WA0000, WA0001, etc. }
  Result := '';
  Str := ExtractFileName(FileName);
  Splitted := Str.Split(['-'], 3);
  If Length(Splitted) = 3 then
  begin
    Prefix := Splitted[0];
    ImageDate := Splitted[1];
    ImageName := Splitted[2];
    if (Prefix.ToUpper.StartsWith('IMG')) and
      (ImageName.ToUpper.StartsWith('WA')) then
      Result := ImageDate.Substring(0, 4) + '-' + ImageDate.Substring(4, 2) +
        '-' + ImageDate.Substring(6, 2)
  end;
end;

function TfrmMain.GetFileType(FileName: string): TFileType;
var
  FileExt: string;
begin
  FileExt := ExtractFileExt(FileName).ToLower;
  if ImageExt.Contains(FileExt) then
    Result := ftImage
  else if RawExt.Contains(FileExt) then
    Result := ftRaw
  else if VideoExt.Contains(FileExt) then
    Result := ftVideo
  else if WebExt.Contains(FileExt) then
    Result := ftWeb
  else if RemoveExt.Contains(FileExt) then
    Result := ftRemove
  else if OtherExt.Contains(FileExt) then
    Result := ftOther
  else
    Result := ftNone;
end;

procedure TfrmMain.lvErrorResize(Sender: TObject);
begin
  lvError.Columns[0].Width := frmMain.Width - 10;
end;

function TfrmMain.CreateDestName(SourceFile, OriginalDate, OriginalYear,
  Directory: string): string;
var
  DestFileName: string;
  PartialFileName: string;
begin
  { all extensions in lowercase and use jpg as uniform file extension for all jpeg images }
  DestFileName := ChangeFileExt(SourceFile, ExtractFileExt(SourceFile)
    .ToLower.Replace('jpeg', 'jpg'));
  if not OriginalYear.IsEmpty then // Shot date is available
  begin
    PartialFileName := '\' + Directory + '\' + OriginalYear + '\' + OriginalDate
      + '\' + ExtractFileName(DestFileName);
    Case GetFileType(SourceFile) of
      ftUndefined:
        DestFileName := '';
      ftImage:
        DestFileName := DestinationDir + ImageDir + PartialFileName;
      ftRaw:
        DestFileName := DestinationDir + RawDir + PartialFileName;
      ftVideo:
        DestFileName := DestinationDir + VideoDir + PartialFileName;
      ftWeb:
        DestFileName := DestinationDir + WebDir + PartialFileName;
      ftRemove:
        DestFileName := DestinationDir + RemoveDir + PartialFileName;
      ftOther:
        DestFileName := DestinationDir + OtherDir + PartialFileName;
      ftNone:
        DestFileName := DestinationDir + OtherDir + PartialFileName;
    End
  end
  else
    Case GetFileType(SourceFile) of
      ftUndefined:
        DestFileName := '';
      ftImage:
        DestFileName := DestinationDir + NoEXIFDir + '\' +
          ExtractFileName(DestFileName);
      ftRaw:
        DestFileName := DestinationDir + NoEXIFDir + '\' +
          ExtractFileName(DestFileName);
      ftVideo:
        DestFileName := DestinationDir + NoEXIFDir + '\' +
          ExtractFileName(DestFileName);
      ftWeb:
        DestFileName := DestinationDir + NoEXIFDir + '\' +
          ExtractFileName(DestFileName);
      ftRemove:
        DestFileName := DestinationDir + RemoveDir + '\' +
          ExtractFileName(DestFileName);
      ftOther:
        DestFileName := DestinationDir + OtherDir + '\' +
          ExtractFileName(DestFileName);
      ftNone:
        DestFileName := DestinationDir + OtherDir + '\' +
          ExtractFileName(DestFileName);
    End;
  Result := DestFileName;
end;

function TfrmMain.GetExifData: TFunctionResult;
var
  cmd: string;
  Itm: TListItem;
  Output, Errors: TStringlist;
  c: integer;
  p: string;

begin
  Output := TStringlist.Create;
  Errors := TStringlist.Create;
  c := -1;

  { EXIFTool works not well with ANSI charset therefore we use UTF8 in file_list.args file
    Selection of the desired EXIF fields is done in btnStartClick and
    put in the first lines of the file_list.args file }
  cmd := 'exiftool ' + // exiftool.exe
    '-d "%Y-%m-%d" ' + // datetimeformat YYYY-MM-DD
    '-j ' + // JSON output
    '-fast2 ' + // most efficient for our goal
    '-charset FileName=UTF8 ' +
  // Filenames in arguments filelist in UTF8 encoding
    '-@ ' + FileListName.QuotedString('"');
  // Arguments filelist including fields to select

  Result := ExecuteExifTool(cmd, Output, Errors);

  case Result of

    frOk:
      begin

        if Errors.Count > 0 then
          for var i := 0 to Errors.Count - 1 do
          begin
            Itm := lvError.Items.Add;
            Itm.Caption := UTF8ToString(RawByteString(Errors[i]));
          end;

        if Output.Count > 0 then
        begin

          CreateReader(UTF8ToString(RawByteString(Output.Text)));
          tblImageProperties.DisableControls;
          while Reader.read do
            case Reader.TokenType of

              TJsonToken.startobject:
                tblImageProperties.Append;

              TJsonToken.StartArray:
                ;

              TJsonToken.PropertyName:
                begin
                  p := Reader.Value.ToString;
                  if p = 'SourceFile' then
                    c := 0; // tblImageProperties.Fields[0]
                  if p = 'FileModifyDate' then
                    c := 1; // tblImageProperties.Fields[1]
                  if p = 'FileCreateDate' then
                    c := 2; // tblImageProperties.Fields[2]
                  if p = 'DateTimeOriginal' then
                    c := 3; // tblImageProperties.Fields[3]
                end; // TJsonToken.PropertyName

              TJsonToken.String:
                begin
                  tblImageProperties.edit;
                  case c of
                    // EXIFTool uses Unix style path names
                    0:
                      tblImageProperties.Fields[c].AsString :=
                        Reader.Value.ToString.Replace('/', '\');
                    1 .. 3:
                      tblImageProperties.Fields[c].AsString :=
                        Reader.Value.ToString;
                  end;
                end; // TJsonToken.String

              TJsonToken.Integer:
                ;
              TJsonToken.Float:
                ;
              TJsonToken.Boolean:
                ;
              TJsonToken.Null:
                ;
              TJsonToken.EndArray:
                ;

              TJsonToken.EndObject:
                begin
                  tblImageProperties.edit;
                  { EXIF data value for DateTimeOriginal available? }
                  if not tblImagePropertiesDateTimeOriginal.AsString.IsEmpty
                  then
                    tblImagePropertiesOriginalDate.AsString :=
                      tblImagePropertiesDateTimeOriginal.AsString
                  else
                  begin
                    { WhatsApp image }
                    tblImagePropertiesOriginalDate.AsString :=
                      GetWhatsAppDate(tblImagePropertiesSourceFile.AsString);
                    if tblImagePropertiesOriginalDate.AsString.IsEmpty then
                    // No WhatsApp Image
                      { then use FileCreateDate, not very accurate but better then nothing. }
                      tblImagePropertiesOriginalDate.AsString :=
                        tblImagePropertiesFileCreateDate.AsString;
                  end;

                  tblImagePropertiesOriginalYear.AsString :=
                    tblImagePropertiesOriginalDate.AsString.Substring(0, 4);

                  tblImagePropertiesDirectory.AsString := // yyy0-yyy9
                    tblImagePropertiesOriginalYear.AsString.Substring(0, 3) +
                    '0' + '-' + tblImagePropertiesOriginalYear.AsString.
                    Substring(0, 3) + '9';

                  tblImagePropertiesDestinationFile.AsString :=
                    CreateDestName(tblImagePropertiesSourceFile.AsString,
                    tblImagePropertiesOriginalDate.AsString,
                    tblImagePropertiesOriginalYear.AsString,
                    tblImagePropertiesDirectory.AsString);

                end; // TJsonToken.EndObject
            end; // case Reader.TokenType
          tblImageProperties.EnableControls;
        end // if Output.Count > 0
      end; // case Result = frOk

    frError:
      ShowMessage('exiftool.exe not found!?');

    frCancel:
      ;
  end; // case Result
end;

procedure TfrmMain.RestructureDirectory;
var
  i: integer;
  DuplicateFiles: integer;
  Guid: TGUID;
  AltDestFileName: string;
begin
  CreateGUID(Guid); // DirectoryName for duplicate files in BaseDir\duplicates
  DuplicateFiles := 0;
  i := 0;
  StatusBar.Panels[5].Text := i.ToString;
  StatusBar.Panels[9].Text := '';

  frmStatus.FilesMoved := i;
  frmStatus.ProcessingFile := '';
  frmStatus.State := stRestructure;
  frmStatus.btnCancel.Enabled := true;
  case rgFileAction.ItemIndex of
    0:
      frmStatus.Caption := 'Performing file moving...';
    1:
      frmStatus.Caption := 'Performing File Copying...';
  end;

  Application.ProcessMessages;
  tblImageProperties.First;
  while not tblImageProperties.eof and not frmStatus.Cancel do
  begin
    Inc(i);
    StatusBar.Panels[5].Text := i.ToString;
    StatusBar.Panels[9].Text :=
      TruncateFileName(tblImagePropertiesSourceFile.AsString,
      StatusBar.Panels[9].Width div 9);

    frmStatus.FilesMoved := i;
    frmStatus.ProcessingFile := tblImagePropertiesSourceFile.AsString;
    frmStatus.Progress := i;

    // Creates a new directory, including the creation of parent directories as needed
    if not System.SysUtils.ForceDirectories
      (ExtractFilePath(tblImagePropertiesDestinationFile.AsString)) then
      raise Exception.Create
        (ExtractFilePath(tblImagePropertiesDestinationFile.AsString));
    if not FileExists(tblImagePropertiesDestinationFile.AsString) then
      case rgFileAction.ItemIndex of
        0:
          TFile.Move(tblImagePropertiesSourceFile.AsString,
            tblImagePropertiesDestinationFile.AsString);
        1:
          TFile.Copy(tblImagePropertiesSourceFile.AsString,
            tblImagePropertiesDestinationFile.AsString);
      end
    else
    begin
      Inc(DuplicateFiles);
      StatusBar.Panels[7].Text := DuplicateFiles.ToString;

      frmStatus.DuplicateFiles := DuplicateFiles;

      AltDestFileName := DestinationDir + 'duplicatesDir' + '\' +
        GUIDToString(Guid) + '\' + Format('%.5d-', [DuplicateFiles]) +
        ExtractFileName(tblImagePropertiesDestinationFile.AsString);
      if not System.SysUtils.ForceDirectories(ExtractFilePath(AltDestFileName))
      then
        raise Exception.Create(ExtractFilePath(AltDestFileName));

      case rgFileAction.ItemIndex of
        0:
          TFile.Move(tblImagePropertiesSourceFile.AsString, AltDestFileName);
        1:
          TFile.Copy(tblImagePropertiesSourceFile.AsString, AltDestFileName);
      end;

      tblImageProperties.edit;
      tblImagePropertiesDuplicate.AsString := AltDestFileName;
      tblImageProperties.Post;
    end;
    Application.ProcessMessages;
    tblImageProperties.Next;
  end; // while not tblImageProperties.eof and not frmStatus.Cancel
  frmStatus.State := stCompleted;
end;

procedure TfrmMain.rgFileActionClick(Sender: TObject);
begin
  frmStatus.FileAction := TFileAction(rgFileAction.ItemIndex);
  if Assigned(frmStatus) then
    StatusBar.Panels[4].Text := FileActionStr[frmStatus.FileAction];
end;

function TfrmMain.Scan(Path: string): TFunctionResult;
{ scans directory for files, recurses for directories found
  Path does not have final backslash }
const
  Success: integer = 0;
var
  f: TSearchRec;
  FileName: string;
begin
  Result := frError;
  if FindFirst(Path + '\*.*', faAnyFile, f) = Success then
  begin
    repeat // until FindNext(f) <> Success
      if (f.Attr and faDirectory) <> 0 then
      begin // is directory
        if (f.Name[1] <> '.') then
        begin // not current or parent directory
          Result := Scan(Path + '\' + f.Name); // recurses all sub-directories
        end; // if fname not .. or . (ie not this or parent directory)
      end // f.attr = faDirectory
      else // // is not directory
      begin
        FileName := Path + '\' + f.Name;
        file_list.Add(FileName);
        Inc(FileCounter);
      end;
      Application.ProcessMessages;
    until (FindNext(f) <> Success) or frmStatus.Cancel;

    if frmStatus.Cancel or (Result = frCancel) then
      Result := frCancel
    else
      Result := frOk;

    System.SysUtils.FindClose(f);

    { Show only final values for better performance }
    StatusBar.Panels[1].Text := FileCounter.ToString;
    StatusBar.Panels[9].Text := TruncateFileName(Path + '\' + f.Name,
      (StatusBar.Panels[9].Width - 10) div 9);
    frmStatus.FilesProcessed := FileCounter;
    frmStatus.ProcessingFile := Path + '\' + f.Name;
  end; // FindFirst(Path + '\*.*', faDirectory, f) = Success
end;

initialization

begin
  FileListName := TL.Components.Folder.LocalAppData +
    '\Langstraat\PicOrg\file_list.args';
  if not System.SysUtils.ForceDirectories(ExtractFilePath(FileListName)) then
    raise Exception.Create('Can not create: ' + FileListName);
end;

end.