Resource

Resource Basics

Resource Name Import CSV Data v2026+
Resource Version 1.62
Category Metadata
Downloads 201
Release Date 2/17/2026 12:29:27 PM
 

Resource Details

Description This script loads XMP data from a CSV file.
Installation Instructions 1. Download the script file
2. Select Tools -> Scripter
3. Load the downloaded script
4. Run the script with the run button
Usage Instructions Start the script. The requirements for the CSV file are:

1. It should use a comma, semi-colon, or TAB delimiter
2. The first line should contain the (also delimited) field definitions
3. There should be at least one column that contains the filename (without path)
Changes in this version 1.32; Fix for dialog size on high-res monitors
1.3; fixed an exception that could be raised when selecting a XMP field
1.4: Added support for importing keywords (dc:subject)
1.41: Importing keywords now respects the Preference setting for Sync-Read
1.42 Fix for double entries in drop down when opening twice
1.43 File name column can now contain the full file path or the file name only
1.44 Fix for MS-Excel formatted CSV files
1.50 Reimplemented Progress
1.52 Fix for reading CSV files with and without BOM leader
1.60 Added support for non-quoted CSV files
1.61 Changed to support PSU v2025
1.62 Changed to support PSU v2026
Special Requirements
 

Author Information

Author Name IDimager Support
 

Available Attachments

Attachment #1 ImportCSV.png
 

Script

(*
' *************************************************************************
' * Script name:
' *   Import CSV Data
' *   Version: 1.62
' *
' * Description:
' *   Imports XMP data from a CSV (delimited) text file to selected images
' *
' * Version History:
' *   IDimager Support
' *     1.0 Initial version
' *     1.1 Fixed an issue with field names
' *     1.2 Added support to import to Custom Fields
' *     1.3 Fixed an issue with Access Violation that could occur
' *     1.31 Fixed an issue with renaming TCatalogItem.idLabel to TCatalogItem.ColorLabel
' *     1.32 Fixed dialog size on high-res monitors
' *     1.40 Added support for importing keywords (dc:subject)
' *     1.41 Importing keywords now respects the Preference setting for Sync-Read
' *     1.42 Fix for double entries in drop down when opening twice
' *     1.43 File name column can now contain the full file path or the file name only
' *     1.44 Fix for MS-Excel formatted CSV files
' *     1.52 Fix for reading CSV files with and without BOM
' *     1.60 Added support for non-quoted CSV files
' *     1.61 Changed to support PSU v2025
' *     1.62 Changed to support PSU v2026
' *
' *************************************************************************/
*)


var
  ACSVLines, ACSVFields: TxomStringList;
  ADelimiter: WideChar;
  AQuoted: Boolean;
  AFirstLineHoldsFieldNames: Boolean;
  AFieldsAreQuoted: Boolean;

  // UI controls
  AFieldComboList: TStringList;
  AFileNameCombo: TvxMacPopupButton;
  AFileEdit: TvxTextBox;
  AContent: TvxMacScrollBox;
  AImportButton: TvxButton;

  function FindValue (AParam: TMacroParam; AVal: WideString): Variant;
  var
    i: Integer;
  begin
    result := AVal;
    if Assigned(AParam) then
    begin
      if AParam.ValidValues.Count > 0 then
      begin
        for i := 0 to AParam.ValidValues.Count - 1 do
        begin
          if WideSameText(AParam.ValidValues.Items[i].ParamName, AVal) then
          begin
            result := AParam.ValidValues.Items[i].Content;
            exit;
          end;
        end;
      end;

      if WideSameText(AParam.ParamCommand, 'xap:Label') or
         WideSameText(AParam.ParamCommand, 'xmp:Label')
      then
      begin
        if WideSameText(Translate('No Label'), result) or
           WideSameText('No Label', result)
        then
          result := '';
      end;
    end;
  end;

  procedure _StartImport(Sender);
  var
    i, j: Integer;
    AFound: Boolean;
    ACatItem: TCatalogItem;
    AMappings: TxomStringList;
    AXmpField, AId: WideString;
    AXmp: TXMP;
    AVal: Variant;
    AGroup, AParam: TMacroParam;
    ASaveItem, AReconstruct: Boolean;
  begin
    if AFileNameCombo.ItemIndex = -1 then exit;

    AMappings := TxomStringList.Create;
    for i := 0 to AFieldComboList.Count - 1 do
    begin
      AXmpField := TvxMacPopupButton(AFieldComboList.Objects[i]).Text;
      if WideSameText(AXmpField, '(don''t link)') then
        AXmpField := '';
      if WideTextPos(':', AXmpField) = 0 then  // xmp fields all have the colon
        AXmpField := '';
      AMappings.Add(AXmpField);
    end;
    if Trim(AMappings.Text) = '' then exit;  // nothing to do

    Progress.Cancel := False;
    Progress.Caption := 'Processing CSV file...';
    Progress.Show;
    Progress.Max := ACSVLines.Count;
    Progress.Pos := 0;

    for i := 0 to ACSVLines.Count - 1 do
    begin
      ALine := TxomStringList.Create;

      if AFieldsAreQuoted then
      begin
        ALine.Delimiter := ADelimiter;
        ALine.DelimitedText := ACSVLines.Strings[i];
      end
      else
        Tokenize(ACSVLines.Strings[i], ADelimiter, ALine);

      AFileName := ALine.Strings[AFileNameCombo.ItemIndex];

      ACatItem := TCatalogItem.Create(nil);
      ACatItem.FileName := AFileName;
      ACatItem.UpdateFileInfoBase;

      // try fnding with the full file name (path+filename)
      AFound := PublicCatalog.FindItemCombined2(ACatItem, ACatItem, False, vptNone);
      if not AFound then
      begin
        // try finding with file name only (no path name)
        AFound := PublicCatalog.FindFileQuick(AFileName, ACatItem, True);
      end;

      if AFound then
      begin
        Progress.Pos := i + 1;
        Progress.ProgressText := AFileName;
        if Progress.Cancel then
          break;

        ASaveItem := False;
        AXmp := TXMP.Create(False);
        PublicCatalog.LoadXMPForItem(ACatItem, AXmp, PublicOptions.CachedXMP);
        for j := 0 to AMappings.Count - 1 do
        begin
          AXmpField := AMappings.Strings[j];
          if AXmpField = '' then
            Continue;

          AId := LeftStr(AXmpField, WideTextPos(':', AXmpField) - 1);
          AGroup := XMPModel.FindId(AId, False);
          if AGroup = nil then  // something unsupported...skip
            Continue;
          AParam := XMPModel.FindCommand(AXmpField, True);
          AVal := FindValue(AParam, ALine.Strings[j]);

          AXmp.QuickSetProperty(AGroup.Content, AXmpField, AVal);

          if WideSameText(AXmpField, 'lr:hierarchicalSubject') then
          begin
            // construct catalog labels for the new keywords
            AReconstruct := True;
            AXmp.RemoveParam('dc:subject');
          end;
          if WideSameText(AXmpField, 'dc:subject') then
          begin
            // construct catalog labels for the new keywords
            AReconstruct := True;
            AXmp.RemoveParam('lr:hierarchicalSubject');
          end;
          if WideSameText(AXmpField, 'xap:Rating') or
             WideSameText(AXmpField, 'xmp:Rating')
          then
          begin
            ACatItem.Rating := AVal;
            ASaveItem := True;
          end;
          if WideSameText(AXmpField, 'xap:Label') or
             WideSameText(AXmpField, 'xmp:Label')
          then
          begin
            ACatItem.ColorLabel := AVal;
            ASaveItem := True;
          end;
        end;
        PublicCatalog.SaveXMPForItem(ACatItem, AXmp, PublicOptions.CachedXMP);
        if ASaveItem then
          PublicCatalog.StoreItemToDatabase(ACatItem, False);
        if AReconstruct then
        begin
          // should keywords be read as catalog labels?
          if PublicOptions.XMPSyncReadSettings.ReplaceLabels <> ksNone then
          begin
            // if keywords should be read, should they replace existing labels or
            // merge? If Replace, then delete what's already there
            if PublicOptions.XMPSyncReadSettings.ReplaceLabels = ksReplace then
              PublicCatalog.RemoveAllPropsFromItem(ACatItem);
            // now create catalog labels from keywords
            PublicCatalog.ReConstructFromXMPObjectKeywords(ACatItem, AXmp, PublicOptions.XMPSyncReadSettings, False);
          end;
        end;

        AXmp.Free;
      end;
      ACatItem.Free;

      ALine.Free;
    end;

    AMappings.Free;

    Progress.Hide;
    if Progress.Cancel then
      Say('Cancelled');
  end;

  procedure _ParamLinkClick(Sender: TObject);
  begin
    // nothing
  end;

  procedure _DontLinkClick(Sender: TObject);
  begin
    // nothing
  end;

  procedure _InitParamsPopup(Sender: TObject);
  var
    AMenu: TvxMacPopupMenu;
    AItem: TvxMacMenuItem;
    i: Integer;
    AParam, AParentParam: TMacroParam;
    ASubs: TMacroParams;
  begin
    AMenu := TvxMacPopupMenu(Sender);
    AMenu.MenuItems.Clear;
    if AMenu.ParentMenuItem = nil then // root level
    begin
      AItem := AMenu.MenuItems.Add;
      AItem.Caption := '(don''t link)';
      AItem.OnClick := '_DontLinkClick';

      AMenu.MenuItems.AddSeparator;
      for i := 0 to XMPModel.Params.Count - 1 do
      begin
        AParam := XMPModel.Params.Items[i];
        if WideSameText(AParam.Id, 'ics') then
          Continue;
        AItem := AMenu.MenuItems.AddSubMenu(AParam.ParamCommand);
        AItem.Tag := AParam;
      end;
    end
    else
    begin
      AParentParam := TMacroParam(AMenu.ParentMenuItem.Tag);
      if AParentParam.ParamType = ptStructure then
        ASubs := AParentParam.CustomParams
      else
        ASubs := AParentParam.SubParams;
      for i := 0 to ASubs.Count - 1 do
      begin
        AParam := ASubs.Items[i];

        if AParam.ParamType = ptStructure then
        begin
          AItem := AMenu.MenuItems.AddSubMenu(AParam.ParamCommand);
          AItem.Tag := AParam;
        end
        else
        begin
          AItem := AMenu.MenuItems.Add;
          AItem.Caption := AParam.ParamCommand;
          AItem.OnClick := '_ParamLinkClick';
        end;
      end;
    end;
  end;

  function CreateBaseCombo(ACaption: WideString; ABold: Boolean): TvxMacPopupButton;
  var
    ALabel: TvxLabel;
  begin
    result := TvxMacPopupButton.Create(AContent);
    result.Width := 180;
    AContent.AddObject(result);

    ALabel := TvxLabel.Create(result);
    ALabel.TextAlign := vgTextAlignFar;
    if ABold then
      ALabel.Font.Style := vgFontBold;
    ALabel.Text := ACaption;
    ALabel.Width := 300;
    ALabel.Position.Point := vgPoint(-310, 5);
    result.AddObject(ALabel);
  end;

  function CreateParamsCombo(ACaption: WideString): TvxMacPopupButton;
  var
    i, j: Integer;
  begin
    result := CreateBaseCombo(ACaption, False);
    result.Text := '(don''t link)';
    result.DynamicContent := True;
    result.OnInitPopup := '_InitParamsPopup';
  end;

  function CreateFieldsCombo(ACaption: WideString): TvxMacPopupButton;
  var
    i: Integer;
  begin
    result := CreateBaseCombo(ACaption, True);
    for i := 0 to ACSVFields.Count - 1 do
      result.Items.Add(ACSVFields.Strings[i]);
  end;

  function FindFileNameField: Integer;
  var
    i: Integer;
    ALine: TxomStringList;
  begin
    result := -1;
    for i := 0 to ACSVFields.Count - 1 do
    begin
      if WideSameText(ACSVFields.Strings[i], 'FileName') or
         WideSameText(ACSVFields.Strings[i], Translate('FileName'))
      then
      begin
        result := i;
        exit;
      end;
    end;

    if ACSVLines.Count > 0 then
    begin
      ALine := TxomStringList.Create;
      ALine.Delimiter := ADelimiter;
      ALine.DelimitedText := ACSVLines.Strings[0];
      for i := 0 to ALine.Count - 1 do
      begin
        if WideFileExists(ALine.Strings[i]) then
        begin
          result := i;
          break;
        end;
      end;
      ALine.Free;
    end;
  end;

  procedure RefreshContent;
  var
    ATop, i: Integer;
    ACombo: TvxMacPopupButton;
    AParam: TMacroParam;
  begin
    AContent.Content.DeleteChildren;
    AFieldComboList.Clear;

    AFileNameCombo := CreateFieldsCombo('Filename Link Column');
    AFileNameCombo.Position.Point := vgPoint(250, 10);
    AFileNameCombo.ItemIndex := FindFileNameField;

    ATop := 40;
    for i := 0 to ACSVFields.Count - 1 do
    begin
      ACombo := CreateParamsCombo('Link "' + ACSVFields.Strings[i] + '" to');
      ACombo.Position.Point := vgPoint(300, ATop);
      ATop := ATop + 25 + 10;
      AParam := XMPModel.FindCommand(ACSVFields.Strings[i], True);
      if Assigned(AParam) then
        ACombo.Text := AParam.ParamCommand;
      ACombo.TagString := ACSVFields.Strings[i];
      ACombo.Tag       := i;
      AFieldComboList.AddObject('', ACombo);
    end;

    AImportButton := TvxButton.Create(AContent);
    AImportButton.Text := 'Start Import';
    AImportButton.Position.Point := vgPoint(10, ATop);
    AImportButton.Font.Size := 14;
    AImportButton.Width := 200;
    AImportButton.Height := 30;
    AImportButton.OnClick := '_StartImport';
    AContent.AddObject(AImportButton);

    AContent.Realign;
  end;

  procedure ReadFields;
  var
    ALine: TxomStringList;
    ACount, i: Integer;
  begin
    ACSVFields.Clear;
    if ACSVLines.Count = 0 then exit;
    ALine := TxomStringList.Create;
    if AFieldsAreQuoted then
    begin
      ALine.Delimiter := ADelimiter;
      ALine.DelimitedText := ACSVLines.Strings[0];
    end
    else
      Tokenize(ACSVLines.Strings[0], ADelimiter, ALine);

    ACount := ALine.Count;
    for i := 0 to ACount - 1 do
    begin
      if AFirstLineHoldsFieldNames then
        ACSVFields.Add(ALine.Strings[i])
      else
        ACSVFields.Add('Field' + IntToStr(i+1));
    end;
    ALine.Free;

    if AFirstLineHoldsFieldNames then
    begin
      // delete the first line
      ACSVLines.Delete(0);
    end;

  end;

  procedure ReadCSVData(AFileName: WideString);
  var
    AFs: TxomFileStream;
    i: Integer;
  begin
    ACSVLines.Clear;
    ACSVFields.Clear;
    if not WideFileExists(AFileName) then exit;

    AFs := TxomFileStream.Create(AFileName, fmOpenRead or fmShareDenyNone);
    AFs.Position := 0;
    ACSVLines.LoadFromStream(AFs);  // auto detect BOM
    AFs.Free;

    // remove empty lines
    for i := ACSVLines.Count - 1 downto 0 do
    begin
      if Trim(ACSVLines.Strings[i]) = '' then
        ACSVLines.Delete(i);

      //ACSVLines.Strings[i] := FormatToProperCSV(ACSVLines.Strings[i], ADelimiter, '"', #0);
    end;

    ReadFields;
  end;

  procedure _OpenButtonClick(Sender: TObject);
  var
    ADlg: TOpenDialog;
  begin
    ADlg := TOpenDialog.Create(nil);
    ADlg.Filter := 'CSV files (*.csv)|*.csv|All files (*.*)|*.*';
    if ADlg.Execute then
    begin
      AFileEdit.Text := ADlg.FileName;
      ReadCSVData(AFileEdit.Text);
      RefreshContent;
    end;
    ADlg.Free;
  end;

  procedure _DelimiterChange(Sender: TObject);
  begin
    if TvxRadioButton(Sender).IsChecked then
    begin
      ADelimiter := TvxRadioButton(Sender).TagString;
      WriteToRegistry('Scripts\ImportCSV', 'Delimiter', ADelimiter);
      if WideFileExists(AFileEdit.Text) then
      begin
        ReadCSVData(AFileEdit.Text);
        RefreshContent;
      end;
    end;
  end;

  procedure _FirstLineFieldNamesChange(Sender: TObject);
  begin
    AFirstLineHoldsFieldNames := TvxCheckBox(Sender).IsChecked;
    WriteToRegistry('Scripts\ImportCSV', 'FirstLineHoldsFieldNames', iif(AFirstLineHoldsFieldNames, 1, 0));
    if WideFileExists(AFileEdit.Text) then
    begin
      ReadCSVData(AFileEdit.Text);
      RefreshContent;
    end;
  end;

  procedure _FieldsAreQuotedChange(Sender: TObject);
  begin
    AFieldsAreQuoted := TvxCheckBox(Sender).IsChecked;
    WriteToRegistry('Scripts\ImportCSV', 'FieldsAreQuoted', iif(AFieldsAreQuoted, 1, 0));
    if WideFileExists(AFileEdit.Text) then
    begin
      ReadCSVData(AFileEdit.Text);
      RefreshContent;
    end;
  end;

  function CreateForm: TvxSceneForm;
  var
    ALayout: TvxLayout;
    AOpenButton: TvxButton;
    ALabel: TvxLabel;
    ADelimiterGroup: TvxGroupBox;
    ARadio: TvxRadioButton;
    ACheck: TvxCheckBox;
  begin
    result := TvxSceneForm.Create(nil);
    result.Caption  := 'Import CSV Data';
    result.Position := poScreenCenter;
    result.Width  := 700 * result.DPIFactor;
    result.Height := 400 * result.DPIFactor;
    result.BorderStyle := bsSizeable;

    ALayout := TvxLayout.Create(result.Root);
    ALayout.Align := vaTop;
    ALayout.Height := 100;
    result.Root.AddObject(ALayout);

    AContent := TvxMacScrollBox.Create(result.Root);
    AContent.Align := vaClient;
    result.Root.AddObject(AContent);

    AFileEdit := TvxTextBox.Create(ALayout);
    AFileEdit.Width := 300;
    AFileEdit.Position.Point := vgPoint(150, 20);
    AFileEdit.TextPadding.Right := AFileEdit.Height + 2;  // don't use the space for the open button
    ALayout.AddObject(AFileEdit);

    ADelimiterGroup := TvxGroupBox.Create(ALayout);
    ADelimiterGroup.Text := 'Delimiter';
    ADelimiterGroup.Height := 85;
    ADelimiterGroup.Position.Point := vgPoint(460, 12);
    ALayout.AddObject(ADelimiterGroup);
    ARadio := TvxRadioButton.Create(ADelimiterGroup);
    ARadio.GroupName := 'delimiter';
    ARadio.Text := 'Comma ,';
    ARadio.TagString := ',';
    ARadio.Position.Point := vgPoint(10, 20);
    ARadio.IsChecked := (ARadio.TagString = ADelimiter);
    ARadio.OnChange := '_DelimiterChange';
    ADelimiterGroup.AddObject(ARadio);
    ARadio := TvxRadioButton.Create(ADelimiterGroup);
    ARadio.GroupName := 'delimiter';
    ARadio.Text := 'Semicolon ;';
    ARadio.TagString := ';';
    ARadio.Position.Point := vgPoint(10, 40);
    ARadio.IsChecked := (ARadio.TagString = ADelimiter);
    ARadio.OnChange := '_DelimiterChange';
    ADelimiterGroup.AddObject(ARadio);
    ARadio := TvxRadioButton.Create(ADelimiterGroup);
    ARadio.GroupName := 'delimiter';
    ARadio.Text := 'TAB';
    ARadio.TagString := #9;
    ARadio.Position.Point := vgPoint(10, 60);
    ARadio.IsChecked := (ARadio.TagString = ADelimiter);
    ARadio.OnChange := '_DelimiterChange';
    ADelimiterGroup.AddObject(ARadio);

    ACheck := TvxCheckBox.Create(ALayout);
    ACheck.Text := 'First line holds column names';
    ACheck.Width := 300;
    ACheck.Position.Point := vgPoint(148, 50);
    ACheck.IsChecked := AFirstLineHoldsFieldNames;
    ACheck.OnChange := '_FirstLineFieldNamesChange';
    ALayout.AddObject(ACheck);

    ACheck := TvxCheckBox.Create(ALayout);
    ACheck.Text := 'Field values are quoted when applicable';
    ACheck.Width := 300;
    ACheck.Position.Point := vgPoint(148, 70);
    ACheck.IsChecked := AFieldsAreQuoted;
    ACheck.OnChange := '_FieldsAreQuotedChange';
    ALayout.AddObject(ACheck);

    ALabel := TvxLabel.Create(AFileEdit);
    ALabel.Text := 'Selected CSV File';
    ALabel.TextAlign := vgTextAlignNear;
    ALabel.Position.Point := vgPoint(-120, 5);
    AFileEdit.AddObject(ALabel);

    AOpenButton := TvxButton.Create(AFileEdit);
    AOpenButton.Align := vaRight;
    AOpenButton.Text := '...';
    AOpenButton.Width := AFileEdit.Height;
    AOpenButton.OnClick := '_OpenButtonClick';
    AFileEdit.AddObject(AOpenButton)
  end;

var
  AForm: TvxSceneForm;
begin
  ADelimiter := ReadFromRegistry('Scripts\ImportCSV', 'Delimiter', ',');
  AFirstLineHoldsFieldNames := ReadFromRegistry('Scripts\ImportCSV', 'FirstLineHoldsFieldNames', 1) = 1;
  AFieldsAreQuoted := ReadFromRegistry('Scripts\ImportCSV', 'FieldsAreQuoted', 1) = 1;

  AFieldComboList := TStringList.Create;
  ACSVLines := TxomStringList.Create;
  ACSVFields := TxomStringList.Create;
  AForm := CreateForm;
  AForm.ShowModal;
  AForm.Free;
  ACSVFields.Free;
  ACSVLines.Free;
  AFieldComboList.Free;
end;