| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- // SPDX-License-Identifier: GPL-3.0-only
- unit UCommandline;
- {$mode objfpc}{$H+}
- interface
- uses classes, LazpaintType, uresourcestrings, LCLStrConsts;
- {$IFDEF WINDOWS}
- {$DEFINE SHOW_MANUAL_IN_WINDOW}
- {$ENDIF}
- const Manual: array[0..79] of string = (
- 'NAME',
- ' LazPaint - Image editor',
- '',
- 'SYNOPSIS',
- ' lazpaint [INPUT FILE] [OUTPUT FILE]',
- ' lazpaint [INPUT FILE] [ACTION]... [OUTPUT FILE]',
- '',
- 'DESCRIPTION',
- ' Graphics viewer and editor.',
- '',
- ' Can read layered files (lzp, ora, pdn, oXo), multi-images (gif, ico,',
- ' tiff), flat files (bmp, jpeg, pcx, png, tga, xpm, xwd), vectorial',
- ' (svg), 3D (obj). Has drawing tools, phong shading, curve adjustments,',
- ' filters and render some textures.',
- '',
- 'OPTIONS',
- ' If supplied, the INPUT FILE is loaded. If the OUTPUT FILE is supplied,',
- ' the image is saved and the program ends. Otherwise, the GUI of the pro‐',
- ' gram is displayed.',
- '',
- ' -scriptbasedir DIRECTORY',
- ' set the directory where Python scripts for LazPaint are located.',
- '',
- ' -script FILENAME',
- ' runs the specified Python script. It must have a ".py" exten‐',
- ' sion.',
- '',
- ' -editor default|CONFIGFILE|OPTION1,OPTION2...',
- ' shows the image editor with validate and cancel buttons. If the',
- ' validate button is used, the rest of the commands are executed.',
- ' Otherwise the program stops.',
- '',
- ' Examples:',
- ' -editor default',
- ' -editor /Users/me/lazpaintCustom.cfg',
- ' -editor [Window]ColorWindowVisible=0,LayerWindowVisible=0',
- '',
- ' -quit',
- ' quits the program even if no output file was provided. Can be',
- ' useful when only running scripts.',
- '',
- ' -new WIDTH,HEIGHT',
- ' creates an empty image of size WIDTH x HEIGHT.',
- '',
- ' -screenshot SCREEN|X1,Y1,X2,Y2',
- ' takes a screenshot of the screen of index SCREEN (0 for primary',
- ' monitor) or of specified coordinates.',
- '',
- ' -resample WIDTH,HEIGHT',
- ' resamples the image to the size WIDTH x HEIGHT.',
- '',
- ' -opacity ALPHA',
- ' applies the opacity to the image. ALPHA is between 0 and 255.',
- '',
- ' -gradient R1,G1,B1,A1,R2,G2,B2,A2,TYPE,X1,Y1,X2,Y2',
- ' renders a gradient from point X1,Y1 to point X2,Y2. TYPE can be',
- ' linear, reflected, diamond, radial or angular. The starting',
- ' color is (R1,G1,B1,A1) and final color is (R2,G2,B2,A2).',
- '',
- ' -horizontalflip',
- ' flips selection or image horizontally.',
- '',
- ' -verticalflip',
- ' flips selection or image vertically.',
- '',
- ' -swapredblue',
- ' swap red and blue channels.',
- '',
- ' -smartzoom3',
- ' resample the image 3 times bigger with smart detection of bor‐',
- ' ders.',
- '',
- ' -rotatecw',
- ' rotates the image clockwise.',
- '',
- ' -rotateccw',
- ' rotates the image counter-clockwise.',
- '',
- ' -rotate180',
- ' rotates the image 180 degrees.');
- procedure ProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList; out errorEncountered, fileSaved, quitQuery: boolean);
- function ParamStrUTF8(AIndex: integer): string;
- implementation
- uses
- SysUtils, BGRAUTF8, LazFileUtils, BGRABitmap, BGRABitmapTypes, BGRALayers, Dialogs, uparse,
- UImage, UImageAction, ULayerAction, UScripting, UPython, Forms, Controls,
- UFileSystem, BGRAIconCursor, UGraph
- {$IFDEF SHOW_MANUAL_IN_WINDOW},StdCtrls{$ENDIF};
- function ParamStrUTF8(AIndex: integer): string;
- begin
- result := SysToUTF8(ParamStr(AIndex)); //not perfect
- end;
- procedure InternalProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList;
- out errorEncountered, fileSaved, quitQuery: boolean; AImageActions: TImageActions);
- var
- commandPrefix: set of char;
- InputFilename:string;
- i,iStart: integer;
- errPos: integer; //number conversion
- //functions
- CommandStr,LowerCmd:string;
- funcParams: ArrayOfString;
- Filter: TPictureFilter;
- //resample
- w,h: integer;
- //opacity
- opacity: byte;
- //gradient
- c1,c2: TBGRAPixel;
- gt: TGradientType;
- o1,o2: TPointF;
- layerAction: TLayerAction;
- enableScript: Boolean;
- function DoGradient: boolean;
- begin
- //c1, c2: TBGRAPixel; gtype: TGradientType; o1, o2: TPointF;
- funcParams := SimpleParseFuncParam(CommandStr);
- if length(funcParams)<>13 then
- begin
- instance.ShowError('Gradient','"Gradient" '+StringReplace(rsExpectNParameters,'N','13',[])+'red1,green1,blue1,alpha1,red2,green2,blue2,alpha2,type,x1,y1,x2,y2');
- errorEncountered := true;
- exit(false);
- end;
- val(funcParams[0],c1.red,errPos);
- val(funcParams[1],c1.green,errPos);
- val(funcParams[2],c1.blue,errPos);
- val(funcParams[3],c1.alpha,errPos);
- val(funcParams[4],c2.red,errPos);
- val(funcParams[5],c2.green,errPos);
- val(funcParams[6],c2.blue,errPos);
- val(funcParams[7],c2.alpha,errPos);
- gt := StrToGradientType(funcParams[8]);
- val(funcParams[9],o1.x,errPos);
- val(funcParams[10],o1.y,errPos);
- val(funcParams[11],o2.x,errPos);
- val(funcParams[12],o2.y,errPos);
- layerAction := instance.Image.CreateAction(true);
- layerAction.DrawingLayer.GradientFill(0,0,
- instance.Image.Width,instance.Image.Height,
- c1,c2,gt,o1,o2,dmDrawWithTransparency,True,False);
- layerAction.Validate;
- FreeAndNil(layerAction);
- result := true;
- end;
- function DoOpacity: boolean;
- begin
- funcParams := SimpleParseFuncParam(CommandStr);
- if length(funcParams)<>1 then
- begin
- instance.ShowError('Opacity','"Opacity" ' + rsExpect1Parameter+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- val(funcParams[0],opacity,errPos);
- if (errPos <> 0) then
- begin
- instance.ShowError('Opacity',rsInvalidOpacity+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- layerAction := instance.Image.CreateAction(true);
- layerAction.DrawingLayer.ApplyGlobalOpacity(opacity);
- layerAction.Validate;
- FreeAndNil(layerAction);
- result := true;
- end;
- function DoResample: boolean;
- begin
- funcParams := SimpleParseFuncParam(CommandStr);
- if length(funcParams)<>2 then
- begin
- instance.ShowError('Resample','"Resample" ' + rsExpect2Parameters+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- val(funcParams[0],w,errPos);
- val(funcParams[1],h,errPos);
- if (errPos <> 0) or (w <= 0) or (h <= 0) then
- begin
- instance.ShowError('Resample',rsInvalidResampleSize+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- instance.Image.Resample(w,h,rfHalfCosine);
- result := true;
- end;
- function DoNew: boolean;
- begin
- funcParams := SimpleParseFuncParam(CommandStr);
- if length(funcParams)<>2 then
- begin
- instance.ShowError('New','"New" ' + rsExpect2Parameters+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- val(funcParams[0],w,errPos);
- val(funcParams[1],h,errPos);
- if (errPos <> 0) or (w <= 0) or (h <= 0) then
- begin
- instance.ShowError('New',rsInvalidSizeForNew+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- instance.Image.Assign(instance.MakeNewBitmapReplacement(w,h,BGRAPixelTransparent),True,False);
- result := true;
- end;
- function DoScreenshot: boolean;
- var r: TRect;
- screenIndex,errPos: integer;
- invalid: boolean;
- begin
- funcParams := SimpleParseFuncParam(CommandStr);
- if length(funcParams)=1 then
- begin
- val(funcParams[0],screenIndex,errPos);
- if errPos <> 0 then
- begin
- instance.ShowError('Screenshot', '"Screenshot" ' + rsExpect1Parameter+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- if (screenIndex < 0) or (screenIndex >= Screen.MonitorCount) then
- begin
- instance.ShowError('Screenshot', '"Screenshot" ' +
- lclstrconsts.rsListIndexExceedsBounds.Replace('%d', inttostr(screenIndex)));
- errorEncountered := true;
- exit(false);
- end;
- r := Screen.Monitors[screenIndex].BoundsRect;
- r := rect(r.Left*CanvasScale, r.Top*CanvasScale,
- r.Right*CanvasScale, r.Bottom*CanvasScale);
- end else
- if length(funcParams)=4 then
- begin
- r := rect(0,0,0,0);
- invalid := false;
- val(funcParams[0],r.Left,errPos);
- if errPos <> 0 then invalid := true;
- val(funcParams[1],r.Top,errPos);
- if errPos <> 0 then invalid := true;
- val(funcParams[2],r.Right,errPos);
- if errPos <> 0 then invalid := true;
- val(funcParams[3],r.Bottom,errPos);
- if errPos <> 0 then invalid := true;
- if invalid then
- begin
- instance.ShowError('Screenshot', '"Screenshot" ' + StringReplace(rsExpectNParameters,'N','4',[])+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- if (errPos <> 0) or (r.Width <= 0) or (r.Height <= 0) then
- begin
- instance.ShowError('New',rsInvalidSizeForNew+IntToStr(r.Width)+'x'+IntToStr(r.Height));
- errorEncountered := true;
- exit(false);
- end;
- end else
- begin
- instance.ShowError('Screenshot', '"Screenshot" ' + StringReplace(rsExpectNParameters,'N','4',[])+CommandStr);
- errorEncountered := true;
- exit(false);
- end;
- AImageActions.TakeScreenshot(r);
- result := true;
- end;
- function MakeConfigFromFuncParam: string;
- var
- cfg: TStringList;
- curSection, newSection, p, param: string;
- begin
- cfg := TStringList.Create;
- curSection := '[General]';
- cfg.Add(curSection);
- try
- for p in funcParams do
- begin
- param := p;
- if param.StartsWith('[') then
- begin
- if param.IndexOf(']') <> -1 then
- begin
- newSection := param.Substring(0, param.IndexOf(']')+1);
- param := param.Substring(length(newSection));
- end else
- begin
- newSection := param;
- param := '';
- end;
- if newSection <> curSection then
- begin
- curSection := newSection;
- cfg.Add(curSection);
- end;
- end;
- if param<>'' then
- begin
- if param.IndexOf('=') = -1 then
- raise Exception.Create(SParExpected.Replace('%s', '"="'));
- cfg.Add(param);
- end;
- end;
- finally
- result := cfg.Text;
- cfg.Free;
- end;
- end;
- function DoEditor: boolean;
- var
- iniStream: TStream;
- bmp: TBGRALayeredBitmap;
- begin
- result := false;
- funcParams := SimpleParseFuncParam(CommandStr);
- if (length(funcParams) = 1) and
- ((ExtractFileExt(funcParams[0])='.ini') or (ExtractFileExt(funcParams[0])='.cfg')) then
- iniStream := FileManager.CreateFileStream(funcParams[0], fmOpenRead)
- else if (length(funcParams) = 1) and (funcParams[0] = 'default') then
- iniStream := TMemoryStream.Create
- else
- iniStream := TStringStream.Create(MakeConfigFromFuncParam);
- bmp := instance.Image.CurrentState.GetLayeredBitmapCopy;
- try
- if instance.EditBitmap(bmp, iniStream) then
- begin
- instance.Image.CurrentState.Assign(bmp, true);
- result := true;
- end
- else
- begin
- bmp.Free;
- quitQuery := true;
- end;
- finally
- FileManager.CancelStreamAndFree(iniStream);
- end;
- end;
- function NextAsFuncParam: boolean;
- begin
- inc(i);
- CommandStr := commandsUTF8[i];
- if (length(CommandStr) >= 1) and (CommandStr[1] in commandPrefix) then
- begin
- instance.ShowError('Command line','Expecting parameters but command found');
- exit(false);
- end;
- result := true;
- end;
- procedure DisplayHelp;
- var
- j: Integer;
- {$IFDEF SHOW_MANUAL_IN_WINDOW}
- f: TForm;
- memo: TMemo;
- {$ENDIF}
- begin
- {$IFDEF SHOW_MANUAL_IN_WINDOW}
- f := TForm.Create(nil);
- try
- f.Caption := rsLazPaint;
- f.Position:= poDesktopCenter;
- f.Width := Screen.Width*3 div 4;
- f.Height := Screen.Height*3 div 4;
- memo := TMemo.Create(f);
- memo.Align:= alClient;
- memo.Parent := f;
- memo.Font.Name:= 'monospace';
- memo.ScrollBars := ssVertical;
- memo.Lines.Clear;
- for j := low(manual) to high(manual) do
- memo.Lines.Add(manual[j]);
- f.ShowModal;
- finally
- f.Free;
- end;
- {$ELSE}
- for j := low(manual) to high(manual) do
- writeln(manual[j]);
- {$ENDIF}
- end;
- procedure DoSaveFile(outputFilename: string);
- var
- icoCur: TBGRAIconCursor;
- stream: TStream;
- ext: String;
- begin
- instance.StartSavingImage(outputFilename);
- try
- ext := ExtractFileExt(outputFilename);
- // normally ICO and CUR cannot be saved directly but make an exception
- if (CompareText(ext, '.ico')=0) or (CompareText(ext, '.cur')=0) then
- begin
- icoCur := TBGRAIconCursor.Create;
- try
- if CompareText(ext, '.cur') = 0 then
- icoCur.FileType := ifCur
- else icoCur.FileType := ifIco;
- icoCur.Add(instance.Image.RenderedImage, BGRABitDepthIconCursor(instance.Image.RenderedImage));
- stream := FileManager.CreateFileStream(outputFilename, fmCreate);
- try
- icoCur.SaveToStream(stream);
- finally
- stream.Free;
- end;
- finally
- icoCur.Free;
- end;
- end else
- instance.Image.SaveToFileUTF8(outputFilename)
- except
- on ex: Exception do
- begin
- instance.ShowError(rsSave, rsUnableToSaveFile+outputFilename);
- end;
- end;
- instance.EndSavingImage;
- end;
- begin
- fileSaved := True;
- quitQuery:= false;
- errorEncountered := false;
- enableScript:= false;
- if commandsUTF8.count = 0 then exit;
- commandPrefix := ['-'];
- {$WARNINGS OFF}
- if PathDelim<>'/' then commandPrefix += ['/'];
- {$WARNINGS ON}
- InputFilename:= commandsUTF8[0];
- iStart := 0;
- if InputFilename <> '' then
- begin
- if not (InputFilename[1] in commandPrefix) then
- begin
- iStart := 1;
- if not FileManager.FileExists(ExpandFileNameUTF8(InputFilename)) then
- begin
- instance.ShowError(rsOpen, rsFileNotFound);
- errorEncountered := true;
- exit;
- end else
- if not instance.TryOpenFileUTF8(ExpandFileNameUTF8(InputFilename), true) then
- begin
- instance.ShowError(rsOpen, rsUnableToLoadFile+InputFilename);
- errorEncountered := true;
- exit;
- end;
- end;
- end;
- fileSaved := false;
- i := iStart-1;
- while i < commandsUTF8.count-1 do
- begin
- inc(i);
- CommandStr := commandsUTF8[i];
- if (length(CommandStr) >= 1) and (CommandStr[1] in commandPrefix) then
- begin
- if (commandStr[1] = '-') and (length(commandStr)>=2) and (commandStr[2] = '-') then
- delete(commandStr,1,2)
- else Delete(CommandStr,1,1);
- Filter := StrToPictureFilter(CommandStr);
- if Filter <> pfNone then
- begin
- if instance.ExecuteFilter(Filter,True) <> srOk then
- begin
- instance.ShowError(CommandStr, rsUnableToApplyFilter+CommandStr);
- errorEncountered := true;
- exit;
- end;
- end else
- begin
- LowerCmd := UTF8LowerCase(CommandStr);
- if (LowerCmd='help') or (LowerCmd = 'h') or (LowerCmd = '?') then
- begin DisplayHelp; quitQuery := true; exit; end else
- if LowerCmd='horizontalflip' then AImageActions.HorizontalFlip(foAuto) else
- if LowerCmd='verticalflip' then AImageActions.VerticalFlip(foAuto) else
- if LowerCmd='swapredblue' then instance.Image.SwapRedBlue else
- if LowerCmd='smartzoom3' then AImageActions.SmartZoom3 else
- if LowerCmd='rotatecw' then AImageActions.RotateCW else
- if LowerCmd='rotateccw' then AImageActions.RotateCCW else
- if LowerCmd='rotate180' then AImageActions.Rotate180 else
- if copy(lowerCmd,1,9)='gradient(' then begin if not DoGradient then exit end else
- if lowerCmd = 'gradient' then begin if not NextAsFuncParam or not DoGradient then exit end else
- if copy(lowerCmd,1,8)='opacity(' then begin if not DoOpacity then exit end else
- if lowerCmd = 'opacity' then begin if not NextAsFuncParam or not DoOpacity then exit end else
- if copy(lowerCmd,1,9)='resample(' then begin if not DoResample then exit end else
- if lowerCmd = 'resample' then begin if not NextAsFuncParam or not DoResample then exit end else
- if copy(lowerCmd,1,4)='new(' then begin if not DoNew then exit end else
- if lowerCmd = 'new' then begin if not NextAsFuncParam or not DoNew then exit end else
- if lowerCmd.StartsWith('screenshot(') then begin if not DoScreenShot then exit end else
- if lowerCmd = 'screenshot' then begin if not NextAsFuncParam or not DoScreenShot then exit end else
- if lowerCmd.StartsWith('editor(') then begin if not DoEditor then exit end else
- if lowerCmd = 'editor' then begin if not NextAsFuncParam or not DoEditor then exit end else
- if lowerCmd = 'script' then
- begin
- enableScript := true;
- end else
- if lowerCmd = 'scriptbasedir' then
- begin
- if not NextAsFuncParam then exit;
- CustomScriptDirectory:= ChompPathDelim(ExpandFileNameUTF8(commandStr));
- end else
- if lowerCmd = 'quit' then
- begin
- quitQuery:= true;
- exit;
- end else
- if Copy(CommandStr,1,4) <> 'psn_' then //ignore mac parameter
- begin
- instance.ShowError('Command line', rsUnknownCommand+CommandStr);
- errorEncountered := true;
- exit;
- end;
- end;
- end else
- if enableScript then
- begin
- ForcePathDelims(commandStr);
- if (CompareText(ExtractFileExt(CommandStr), '.py') <> 0) then
- begin
- instance.ShowError('Command line', rsFileExtensionNotSupported + ' ('+ExtractFileExt(commandStr)+')');
- errorEncountered:= true;
- exit;
- end;
- if not FileExistsUTF8(commandStr) and
- (pos(PathDelim, commandStr) = 0) then
- commandStr := TPythonScript.DefaultScriptDirectory + PathDelim + commandStr;
- if not instance.RunScript(commandStr) then exit;
- enableScript := false;
- end else
- begin
- ForcePathDelims(CommandStr);
- DoSaveFile(commandStr);
- fileSaved:= true;
- exit;
- end;
- end;
- end;
- procedure ProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList;
- out errorEncountered, fileSaved, quitQuery: boolean);
- begin
- InternalProcessCommands(instance, commandsUTF8, errorEncountered, fileSaved, quitQuery, TImageActions(instance.ImageAction));
- end;
- end.
|