Explorar o código

duplicate frame in GIF or TIFF

circular17 %!s(int64=6) %!d(string=hai) anos
pai
achega
b52611f411

+ 1 - 0
lazpaint/dialog/umultiimage.pas

@@ -111,6 +111,7 @@ begin
       result.bmp := images[selectedIndex].bmp.Duplicate(AFormat = ifCur,True) as TBGRABitmap;
       result.bpp := images[selectedIndex].bpp;
       result.frameIndex := images[selectedIndex].frameIndex;
+      result.isDuplicate:= images[selectedIndex].isDuplicate;
     end;
   end;
 end;

+ 16 - 2
lazpaint/dialog/upreviewdialog.pas

@@ -20,9 +20,11 @@ type
     procedure FormDestroy(Sender: TObject);
   private
     FPreview: TImagePreview;
+    function GetDuplicateSourceIndex: integer;
     function GetEntryCount: integer;
     function GetFilename: string;
     function GetLazPaintInstance: TLazPaintCustomInstance;
+    procedure SetDuplicateSourceIndex(AValue: integer);
     procedure SetFilename(AValue: string);
     procedure PreviewValidate(Sender: TObject);
     procedure PreviewEscape(Sender: TObject);
@@ -32,21 +34,23 @@ type
     property Filename: string read GetFilename write SetFilename;
     property LazPaintInstance: TLazPaintCustomInstance read GetLazPaintInstance write SetLazPaintInstance;
     property EntryCount: integer read GetEntryCount;
+    property DuplicateSourceIndex: integer read GetDuplicateSourceIndex write SetDuplicateSourceIndex;
   end;
 
 var
   FPreviewDialog: TFPreviewDialog;
 
 function ShowPreviewDialog(AInstance: TLazPaintCustomInstance; AFilename: string; ATitle: string = '';
-  ASkipIfSingleImage: boolean = false): TImageEntry;
+  ASkipIfSingleImage: boolean = false; ADuplicateSourceIndex: integer = -1): TImageEntry;
 
 implementation
 
 function ShowPreviewDialog(AInstance: TLazPaintCustomInstance; AFilename: string; ATitle: string;
-  ASkipIfSingleImage: boolean): TImageEntry;
+  ASkipIfSingleImage: boolean; ADuplicateSourceIndex: integer): TImageEntry;
 var f: TFPreviewDialog;
 begin
   f := TFPreviewDialog.Create(nil);
+  f.DuplicateSourceIndex := ADuplicateSourceIndex;
   f.LazPaintInstance := AInstance;
   if ATitle <> '' then f.Caption := ATitle;
   f.Filename:= AFilename;
@@ -90,11 +94,21 @@ begin
     result := 0;
 end;
 
+function TFPreviewDialog.GetDuplicateSourceIndex: integer;
+begin
+  result := FPreview.DuplicateEntrySourceIndex;
+end;
+
 function TFPreviewDialog.GetLazPaintInstance: TLazPaintCustomInstance;
 begin
   result := FPreview.LazPaintInstance;
 end;
 
+procedure TFPreviewDialog.SetDuplicateSourceIndex(AValue: integer);
+begin
+  FPreview.DuplicateEntrySourceIndex:= AValue;
+end;
+
 procedure TFPreviewDialog.SetFilename(AValue: string);
 begin
   FPreview.Filename := AValue;

+ 29 - 8
lazpaint/lazpaintmainform.pas

@@ -836,8 +836,9 @@ type
 
     procedure PaintPictureNow;
     procedure InvalidatePicture;
-    function TryOpenFileUTF8(filenameUTF8: string; AddToRecent: Boolean=True; ALoadedImage: PImageEntry = nil;
-      ASkipDialogIfSingleImage: boolean = false): Boolean;
+    function TryOpenFileUTF8(filenameUTF8: string; AddToRecent: Boolean=True;
+      ALoadedImage: PImageEntry = nil; ASkipDialogIfSingleImage: boolean = false;
+      AAllowDuplicate: boolean = false): Boolean;
     function PictureCanvasOfs: TPoint;
     procedure UpdateLineCapBar;
     procedure UpdateColorToolbar(AUpdateColorDiff: boolean);
@@ -1297,7 +1298,8 @@ begin
     if AVars.IsReferenceDefined(vFileName) then
     begin
       FLazPaintInstance.ShowTopmost(topInfo);
-      if TryOpenFileUTF8(AVars.GetString(vFilename), true, nil) then
+      if TryOpenFileUTF8(AVars.GetString(vFilename), true, nil,
+           false, AVars.Booleans['AllowDuplicate']) then
         result := srOk
       else
         result := srException;
@@ -3023,6 +3025,7 @@ var
 begin
   openParams := TVariableSet.Create('FileOpen');
   openParams.AddString('FileName',Image.currentFilenameUTF8);
+  openParams.AddBoolean('AllowDuplicate',true);
   Scripting.CallScriptFunction(openParams);
   openParams.Free;
 end;
@@ -3547,10 +3550,12 @@ begin
 end;
 
 function TFMain.TryOpenFileUTF8(filenameUTF8: string; AddToRecent: Boolean;
-     ALoadedImage: PImageEntry; ASkipDialogIfSingleImage: boolean): Boolean;
+     ALoadedImage: PImageEntry; ASkipDialogIfSingleImage: boolean;
+     AAllowDuplicate: boolean): Boolean;
 var
   newPicture: TImageEntry;
   format: TBGRAImageFormat;
+  dupIndex: Integer;
 
   procedure StartImport;
   begin
@@ -3631,14 +3636,30 @@ begin
     else
     if format in[ifIco,ifTiff] then
     begin
-      newPicture := ShowPreviewDialog(LazPaintInstance, FilenameUTF8, 'TIFF', ASkipDialogIfSingleImage);
-      ImportNewPicture;
+      if (format = ifTiff) and AAllowDuplicate and (Image.FrameIndex <> -1) then dupIndex := Image.FrameIndex else dupIndex := -1;
+      newPicture := ShowPreviewDialog(LazPaintInstance, FilenameUTF8, 'TIFF',
+        ASkipDialogIfSingleImage, dupIndex);
+      if newPicture.isDuplicate then
+      begin
+        newPicture.FreeAndNil;
+        Image.FrameIndex:= newPicture.frameIndex;
+        Image.OnImageChanged.NotifyObservers;
+      end
+      else ImportNewPicture;
     end
     else
     if format = ifGif then
     begin
-      newPicture := ShowPreviewDialog(LazPaintInstance, FilenameUTF8, rsAnimatedGIF, ASkipDialogIfSingleImage);
-      ImportNewPicture;
+      if AAllowDuplicate and (Image.FrameIndex <> -1) then dupIndex := Image.FrameIndex else dupIndex := -1;
+      newPicture := ShowPreviewDialog(LazPaintInstance, FilenameUTF8, rsAnimatedGIF,
+        ASkipDialogIfSingleImage, dupIndex);
+      if newPicture.isDuplicate then
+      begin
+        newPicture.FreeAndNil;
+        Image.FrameIndex:= newPicture.frameIndex;
+        Image.OnImageChanged.NotifyObservers;
+      end
+      else ImportNewPicture;
     end else
     begin
       StartImport;

+ 2 - 0
lazpaint/lazpainttype.pas

@@ -118,6 +118,7 @@ type
       bmp: TBGRABitmap;
       bpp: integer;
       frameIndex: integer;
+      isDuplicate: boolean;
       class function Empty: TImageEntry; static;
       class function NewFrameIndex: integer; static;
       procedure FreeAndNil;
@@ -527,6 +528,7 @@ begin
   result.bmp := nil;
   result.bpp := 0;
   result.frameIndex := 0;
+  result.isDuplicate:= false;
 end;
 
 class function TImageEntry.NewFrameIndex: integer;

+ 4 - 0
lazpaint/release/bin/i18n/lazpaint.de.po

@@ -1729,6 +1729,10 @@ msgstr ""
 msgid "Dither layer using palette"
 msgstr "Dither auf Palette dieser Ebene anwenden"
 
+#: uresourcestrings.rsduplicateimage
+msgid "Duplicate image"
+msgstr "Bild duplizieren"
+
 #: uresourcestrings.rseditmask
 msgid "Edit mask"
 msgstr "Maske bearbeiten"

+ 4 - 0
lazpaint/release/bin/i18n/lazpaint.es.po

@@ -1698,6 +1698,10 @@ msgstr "El directorio no esta vacío"
 msgid "Dither layer using palette"
 msgstr "Entramado de capa usando paleta"
 
+#: uresourcestrings.rsduplicateimage
+msgid "Duplicate image"
+msgstr "Duplicar imagen"
+
 #: uresourcestrings.rseditmask
 msgctxt "uresourcestrings.rseditmask"
 msgid "Edit mask"

+ 4 - 0
lazpaint/release/bin/i18n/lazpaint.fr.po

@@ -1720,6 +1720,10 @@ msgstr "Le dossier n'est pas vide"
 msgid "Dither layer using palette"
 msgstr "Tramer le calque en utilisant la palette"
 
+#: uresourcestrings.rsduplicateimage
+msgid "Duplicate image"
+msgstr "Dupliquer l'image"
+
 #: uresourcestrings.rseditmask
 msgctxt "uresourcestrings.rseditmask"
 msgid "Edit mask"

+ 4 - 0
lazpaint/release/bin/i18n/lazpaint.po

@@ -1684,6 +1684,10 @@ msgstr ""
 msgid "Dither layer using palette"
 msgstr ""
 
+#: uresourcestrings.rsduplicateimage
+msgid "Duplicate image"
+msgstr ""
+
 #: uresourcestrings.rseditmask
 msgid "Edit mask"
 msgstr ""

+ 4 - 0
lazpaint/release/bin/i18n/lazpaint.pt_BR.po

@@ -1706,6 +1706,10 @@ msgstr ""
 msgid "Dither layer using palette"
 msgstr ""
 
+#: uresourcestrings.rsduplicateimage
+msgid "Duplicate image"
+msgstr "Duplicar imagem"
+
 #: uresourcestrings.rseditmask
 msgid "Edit mask"
 msgstr "Editar máscara"

+ 1 - 0
lazpaint/release/changelog

@@ -104,6 +104,7 @@ lazpaint (7.0.7) stable; urgency=low
   * fix transform when copying shapes between layers
   * fix crop layer with layer offset
   * clear layer when deleting it in shape editor
+  * duplicate entry in GIF or TIFF
 
 -- circular <[email protected]>  Wed, 10 Oct 2019 17:37:00 +0100
 

+ 127 - 23
lazpaint/uimagepreview.pas

@@ -33,13 +33,14 @@ type
     FTiff: TTiff;                     //has entries
     FIconCursor: TBGRAIconCursor;     //has entries
     FThumbnails: TBGRABitmapList;
+    FDuplicateEntrySourceIndex: integer;
 
     FSelectedMenuIndex: integer;
     FImageMenu: array of record
                  Area, IconArea: TRect;
                  DeleteArea: TRect;
                  FrameIndex: integer;
-                 IsNew,IsLoopCount: boolean;
+                 IsNew,IsDuplicate,IsLoopCount: boolean;
                end;
 
     FOnValidate: TNotifyEvent;
@@ -63,6 +64,7 @@ type
     procedure ScrollToSelectedMenu;
 
     function CanAddNewEntry: boolean;
+    function CanDuplicateEntry: boolean;
     function CanDeleteEntry(index: integer): boolean;
     procedure DeleteEntry(i: integer);
     function GetEntryCount: integer;
@@ -70,6 +72,7 @@ type
     function GetEntryHeight(index: integer): integer;
     function GetEntryBitDepth(index: integer): integer;
     function GetEntryBitmap(index: integer): TImageEntry;
+    procedure SetEntryBitmap(var AEntry: TImageEntry);
     function GetEntryThumbnail(index: integer; stretchWidth, stretchHeight: integer): TBGRABitmap;
 
     procedure DrawCurrentFrame(Bitmap: TBGRABitmap);
@@ -91,6 +94,7 @@ type
     property OnEscape: TNotifyEvent read FOnEscape write FOnEscape;
     property EntryCount: integer read GetEntryCount;
     function GetPreviewBitmap: TImageEntry;
+    property DuplicateEntrySourceIndex: integer read FDuplicateEntrySourceIndex write FDuplicateEntrySourceIndex;
   end;
 
 implementation
@@ -265,7 +269,8 @@ begin
   begin
     Key := 0;
     if (FSelectedMenuIndex >= 0) and (FSelectedMenuIndex <= high(FImageMenu)) and
-      not FImageMenu[FSelectedMenuIndex].IsNew then
+      not FImageMenu[FSelectedMenuIndex].IsNew and
+      not FImageMenu[FSelectedMenuIndex].IsDuplicate then
     begin
       DeleteEntry(FImageMenu[FSelectedMenuIndex].FrameIndex);
     end;
@@ -309,16 +314,31 @@ begin
 end;
 
 procedure TImagePreview.DrawMenu(Bitmap: TBGRABitmap);
+
+  procedure DrawSheet(x,y,sw,sh: single);
+  var
+    ptsF,ptsF2: ArrayOfTPointF;
+    j: integer;
+  begin
+    ptsF := PointsF([PointF(x+sw*0.20,y+sh*0.1),PointF(x+sw*0.55,y+sh*0.1),PointF(x+sw*0.75,y+sh*0.3),
+                     PointF(x+sw*0.75,y+sh*0.9),PointF(x+sw*0.20,y+sh*0.9)]);
+    setlength(ptsF2,length(ptsF));
+    for j := 0 to high(ptsF) do
+        ptsF2[j] := ptsF[j] + PointF(3,3);
+    bitmap.FillPolyAntialias(ptsF2, BGRA(0,0,0,96));
+    bitmap.FillPolyAntialias(ptsF, BGRAWhite);
+    bitmap.DrawPolygonAntialias(ptsF, BGRABlack, 1.5);
+    bitmap.DrawPolyLineAntialias([PointF(x+sw*0.55,y+sh*0.1),PointF(x+sw*0.55,y+sh*0.3),PointF(x+sw*0.75,y+sh*0.3)], BGRABlack,1.5);
+  end;
+
 var scrollPos, totalHeight, maxScroll, availableWidth: integer;
-  i,j: integer;
+  i: integer;
   x,y,sw,sh: integer;
   textRight, bpp: integer;
   iconCaption: string;
-  ptsF,ptsF2: ArrayOfTPointF;
   scrolledArea, inter: TRect;
 begin
   if (Bitmap.Width < 8) or (Bitmap.Height < 8) or (GetEntryCount = 0) then exit;
-
   if Assigned(FScrollbar) then
   begin
     scrollPos := FScrollbar.Position;
@@ -364,10 +384,12 @@ begin
     FSelectedMenuIndex:= -1;
   if (FSelectedMenuIndex = -1) and (length(FImageMenu) > 0) then
   begin
-    if (length(FImageMenu)>=2) and FImageMenu[0].IsNew then
-      FSelectedMenuIndex:= 1 //do not select "add new" entry by default
-    else
-      FSelectedMenuIndex:= 0;
+    FSelectedMenuIndex:= 0;
+    while (FSelectedMenuIndex < length(FImageMenu)) and
+      (FImageMenu[FSelectedMenuIndex].IsNew or FImageMenu[FSelectedMenuIndex].IsDuplicate
+       or FImageMenu[FSelectedMenuIndex].IsLoopCount) do
+      inc(FSelectedMenuIndex);
+    //do not select special entries by default
   end;
 
   for i := 0 to high(FImageMenu) do
@@ -382,7 +404,7 @@ begin
     if i = FSelectedMenuIndex then
     begin
       bitmap.FillRect(scrolledArea, ColorToRGB(clHighlight));
-      if not IsNew and not IsLoopCount and (Area.Right - IconArea.Right > 32) and CanDeleteEntry(FrameIndex) then
+      if not IsNew and not IsLoopCount and not IsDuplicate and (Area.Right - IconArea.Right > 32) and CanDeleteEntry(FrameIndex) then
       begin
         sh := (Area.Right - IconArea.Right - 8) div 4;
         if sh < 16 then sh := 16;
@@ -416,15 +438,13 @@ begin
     end else
     if IsNew then
     begin
-      ptsF := PointsF([PointF(x+sw*0.20,y+sh*0.1),PointF(x+sw*0.55,y+sh*0.1),PointF(x+sw*0.75,y+sh*0.3),
-                       PointF(x+sw*0.75,y+sh*0.9),PointF(x+sw*0.20,y+sh*0.9)]);
-      setlength(ptsF2,length(ptsF));
-      for j := 0 to high(ptsF) do
-          ptsF2[j] := ptsF[j] + PointF(3,3);
-      bitmap.FillPolyAntialias(ptsF2, BGRA(0,0,0,96));
-      bitmap.FillPolyAntialias(ptsF, BGRAWhite);
-      bitmap.DrawPolygonAntialias(ptsF, BGRABlack, 1.5);
-      bitmap.DrawPolyLineAntialias([PointF(x+sw*0.55,y+sh*0.1),PointF(x+sw*0.55,y+sh*0.3),PointF(x+sw*0.75,y+sh*0.3)], BGRABlack,1.5);
+      DrawSheet(x,y,sw,sh);
+    end else
+    if IsDuplicate then
+    begin
+      DrawSheet(x-sw*0.15,y-sh*0.1,sw,sh*0.9);
+      DrawSheet(x+sw*0.1,y+sh*0.1,sw,sh*0.9);
+      bitmap.FontFullHeight:= round(sh*0.7);
     end else
     begin
       bitmap.FillRect(rect(x+2,y+2, x+sw+2,y+sh+2), BGRA(0,0,0,96), dmDrawWithTransparency);
@@ -432,6 +452,7 @@ begin
     end;
 
     if IsNew then iconCaption := rsNewImage else
+    if IsDuplicate then iconCaption := rsDuplicateImage else
     if IsLoopCount then
     begin
       iconCaption:= rsLoopCount+': ';
@@ -466,7 +487,8 @@ end;
 
 function TImagePreview.TryMenuLayout(AWidth: integer; AColCount, ABottom: integer): integer;
 var x,y,i,frameIndex,h,w,sw,sh: integer;
-  newItem, LoopCountItem, colLeft,colRight, maxWidth, maxHeight: integer;
+  newItem, LoopCountItem, DuplicateItem,
+  colLeft,colRight, maxWidth, maxHeight: integer;
   currentCol: integer;
 
   procedure ComputeColumn;
@@ -490,8 +512,9 @@ begin
 
   if Assigned(FAnimatedGif) then LoopCountItem := 1 else LoopCountItem:= 0;
   if CanAddNewEntry then NewItem := 1 else NewItem := 0;
-  setlength(FImageMenu, GetEntryCount + NewItem + LoopCountItem);
-  for i := 0 to GetEntryCount-1 + NewItem + LoopCountItem do
+  if CanDuplicateEntry then DuplicateItem := 1 else DuplicateItem := 0;
+  setlength(FImageMenu, GetEntryCount + LoopCountItem + NewItem + DuplicateItem);
+  for i := 0 to high(FImageMenu) do
   begin
     if (LoopCountItem = 1) and (i = 0) then
     begin
@@ -508,8 +531,16 @@ begin
       h := 32;
     end
     else
+    if (DuplicateItem = 1) and (i = LoopCountItem + NewItem) then
     begin
-      frameIndex := i-NewItem-LoopCountItem;
+      frameIndex := GetEntryCount;
+      FImageMenu[i].IsDuplicate := true;
+      w := 32;
+      h := 32;
+    end
+    else
+    begin
+      frameIndex := i-NewItem-LoopCountItem-DuplicateItem;
       w := GetEntryWidth(frameIndex);
       h := GetEntryHeight(frameIndex);
     end;
@@ -549,6 +580,12 @@ begin
   result := Assigned(FIconCursor) or Assigned(FTiff) or Assigned(FAnimatedGif);
 end;
 
+function TImagePreview.CanDuplicateEntry: boolean;
+begin
+  result := (Assigned(FTiff) or Assigned(FAnimatedGif)) and
+    (FDuplicateEntrySourceIndex >= 0) and (FDuplicateEntrySourceIndex < EntryCount);
+end;
+
 function TImagePreview.GetEntryCount: integer;
 begin
   if Assigned(FIconCursor) then
@@ -641,6 +678,56 @@ begin
   end;
 end;
 
+procedure TImagePreview.SetEntryBitmap(var AEntry: TImageEntry);
+var
+  sAddedTiff: TMemoryStream;
+  addedTiff: TTiff;
+  sOut: TStream;
+begin
+  if (AEntry.frameIndex < 0) or (AEntry.frameIndex > GetEntryCount) then
+    raise exception.Create('Index out of bounds');
+  if Filename = '' then raise exception.create('Filename undefined');
+
+  if Assigned(FTiff) then
+  begin
+    addedTiff := TTiff.Create;
+    sAddedTiff := TMemoryStream.Create;
+    try
+      AEntry.bmp.SaveToStreamAs(sAddedTiff, ifTiff);
+      sAddedTiff.Position:= 0;
+      if addedTiff.LoadFromStream(sAddedTiff) <> teNone then
+        raise Exception.Create(rsInternalError);
+      if AEntry.frameIndex > FTiff.Count then
+        AEntry.frameIndex := FTiff.Count;
+      FTiff.Move(addedTiff,0, AEntry.frameIndex);
+
+      sOut := FileManager.CreateFileStream(Filename,fmCreate);
+      try
+        FTiff.SaveToStream(sOut);
+      finally
+        sOut.Free;
+      end;
+    finally
+      sAddedTiff.Free;
+      addedTiff.Free;
+    end;
+  end else
+  if Assigned(FAnimatedGif) then
+  begin
+    if AEntry.frameIndex >= FAnimatedGif.Count then
+      AEntry.frameIndex := FAnimatedGif.AddFullFrame(AEntry.bmp, FAnimatedGif.AverageDelayMs)
+    else
+      FAnimatedGif.ReplaceFullFrame(AEntry.frameIndex, AEntry.bmp, FAnimatedGif.FrameDelayMs[AEntry.frameIndex]);
+
+    sOut := FileManager.CreateFileStream(Filename,fmCreate);
+    try
+      FAnimatedGif.SaveToStream(sOut);
+    finally
+      sOut.Free;
+    end;
+  end;
+end;
+
 function TImagePreview.GetEntryThumbnail(index: integer; stretchWidth, stretchHeight: integer): TBGRABitmap;
 var
   entry: TImageEntry;
@@ -980,6 +1067,7 @@ begin
   FStatus := AStatus;
   FAnimate:= AAnimate;
   FSelectedMenuIndex := -1;
+  FDuplicateEntrySourceIndex := -1;
   {$IFDEF WINDOWS}
   ASurface.Color := clAppWorkspace;
   {$ENDIF}
@@ -1095,6 +1183,14 @@ begin
             result.frameIndex:= TImageEntry.NewFrameIndex;
           end;
         end else
+        if FImageMenu[FSelectedMenuIndex].IsDuplicate then
+        begin
+          result := GetEntryBitmap(DuplicateEntrySourceIndex);
+          result.frameIndex:= GetEntryCount;
+          result.isDuplicate:= true;
+          SetEntryBitmap(result);
+        end
+        else
           result := GetEntryBitmap(FImageMenu[FSelectedMenuIndex].FrameIndex);
       end;
     end else
@@ -1122,6 +1218,14 @@ begin
               result.bmp := TBGRABitmap.Create(FAnimatedGif.Width,FAnimatedGif.Height,BGRAPixelTransparent);
               result.frameIndex:= TImageEntry.NewFrameIndex;
           end else
+          if FImageMenu[FSelectedMenuIndex].IsDuplicate then
+          begin
+            result := GetEntryBitmap(DuplicateEntrySourceIndex);
+            result.frameIndex:= GetEntryCount;
+            result.isDuplicate:= true;
+            SetEntryBitmap(result);
+          end
+          else
             result := GetEntryBitmap(FImageMenu[FSelectedMenuIndex].FrameIndex);
         end;
       end else

+ 1 - 0
lazpaint/uresourcestrings.pas

@@ -63,6 +63,7 @@ resourcestring
   rsMergeSelection='Do you want to merge selection?';
   rsSave='Save';
   rsNewImage='New image';
+  rsDuplicateImage='Duplicate image';
   rsOpen='Open';
   rsReload='Reload';
   rsReloadChanged='Bitmap has been modified. Do you really want to reload?';

+ 22 - 0
lazpaint/uscripting.pas

@@ -51,6 +51,7 @@ type
     function GetSubsetByName(const AName: string): TVariableSet;
     function GetListByName(const AName: string): string;
     function GetVariablesAsString: string;
+    function GetVarName(AIndex: integer): string;
     procedure SetBooleanByName(const AName: string; AValue: boolean);
     procedure SetFloatByName(const AName: string; AValue: double);
     procedure SetIntegerByName(const AName: string; AValue: TScriptInteger);
@@ -126,6 +127,7 @@ type
     function CopyValuesTo(ASet: TVariableSet): boolean;
     property FunctionName: string read FFunctionName;
     property Count: NativeInt read GetCount;
+    property VariableName[AIndex: integer]: string read GetVarName;
     property VariablesAsString: string read GetVariablesAsString;
     property Floats[const AName: string]: double read GetFloatByName write SetFloatByName;
     property Integers[const AName: string]: TScriptInteger read GetIntegerByName write SetIntegerByName;
@@ -386,6 +388,26 @@ begin
 
 end;
 
+function TVariableSet.GetVarName(AIndex: integer): string;
+begin
+  if AIndex < 0 then raise exception.Create('Index out of bounds');
+
+  if AIndex < FNbScalars then exit(FScalars[AIndex].name)
+  else dec(AIndex, FNbScalars);
+  if AIndex < FNbStrings then exit(FStrings[AIndex].name)
+  else dec(AIndex, FNbStrings);
+  if AIndex < FNbBoolLists then exit(FBoolLists[AIndex].name)
+  else dec(AIndex, FNbBoolLists);
+  if AIndex < FNbScalarLists then exit(FScalarLists[AIndex].name)
+  else dec(AIndex, FNbScalarLists);
+  if AIndex < FNbStrLists then exit(FStrLists[AIndex].name)
+  else dec(AIndex, FNbStrLists);
+  if AIndex < FNbSubsets then exit(FSubsets[AIndex].name)
+  else dec(AIndex, FNbSubsets);
+
+  raise exception.Create('Index out of bounds');
+end;
+
 function TVariableSet.LoadFromVariablesAsString(AVariablesAsString: string
   ): TInterpretationErrors;
 var varName: string;