ucommandline.pas 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. // SPDX-License-Identifier: GPL-3.0-only
  2. unit UCommandline;
  3. {$mode objfpc}{$H+}
  4. interface
  5. uses classes, LazpaintType, uresourcestrings, LCLStrConsts;
  6. {$IFDEF WINDOWS}
  7. {$DEFINE SHOW_MANUAL_IN_WINDOW}
  8. {$ENDIF}
  9. const Manual: array[0..79] of string = (
  10. 'NAME',
  11. ' LazPaint - Image editor',
  12. '',
  13. 'SYNOPSIS',
  14. ' lazpaint [INPUT FILE] [OUTPUT FILE]',
  15. ' lazpaint [INPUT FILE] [ACTION]... [OUTPUT FILE]',
  16. '',
  17. 'DESCRIPTION',
  18. ' Graphics viewer and editor.',
  19. '',
  20. ' Can read layered files (lzp, ora, pdn, oXo), multi-images (gif, ico,',
  21. ' tiff), flat files (bmp, jpeg, pcx, png, tga, xpm, xwd), vectorial',
  22. ' (svg), 3D (obj). Has drawing tools, phong shading, curve adjustments,',
  23. ' filters and render some textures.',
  24. '',
  25. 'OPTIONS',
  26. ' If supplied, the INPUT FILE is loaded. If the OUTPUT FILE is supplied,',
  27. ' the image is saved and the program ends. Otherwise, the GUI of the pro‐',
  28. ' gram is displayed.',
  29. '',
  30. ' -scriptbasedir DIRECTORY',
  31. ' set the directory where Python scripts for LazPaint are located.',
  32. '',
  33. ' -script FILENAME',
  34. ' runs the specified Python script. It must have a ".py" exten‐',
  35. ' sion.',
  36. '',
  37. ' -editor default|CONFIGFILE|OPTION1,OPTION2...',
  38. ' shows the image editor with validate and cancel buttons. If the',
  39. ' validate button is used, the rest of the commands are executed.',
  40. ' Otherwise the program stops.',
  41. '',
  42. ' Examples:',
  43. ' -editor default',
  44. ' -editor /Users/me/lazpaintCustom.cfg',
  45. ' -editor [Window]ColorWindowVisible=0,LayerWindowVisible=0',
  46. '',
  47. ' -quit',
  48. ' quits the program even if no output file was provided. Can be',
  49. ' useful when only running scripts.',
  50. '',
  51. ' -new WIDTH,HEIGHT',
  52. ' creates an empty image of size WIDTH x HEIGHT.',
  53. '',
  54. ' -screenshot SCREEN|X1,Y1,X2,Y2',
  55. ' takes a screenshot of the screen of index SCREEN (0 for primary',
  56. ' monitor) or of specified coordinates.',
  57. '',
  58. ' -resample WIDTH,HEIGHT',
  59. ' resamples the image to the size WIDTH x HEIGHT.',
  60. '',
  61. ' -opacity ALPHA',
  62. ' applies the opacity to the image. ALPHA is between 0 and 255.',
  63. '',
  64. ' -gradient R1,G1,B1,A1,R2,G2,B2,A2,TYPE,X1,Y1,X2,Y2',
  65. ' renders a gradient from point X1,Y1 to point X2,Y2. TYPE can be',
  66. ' linear, reflected, diamond, radial or angular. The starting',
  67. ' color is (R1,G1,B1,A1) and final color is (R2,G2,B2,A2).',
  68. '',
  69. ' -horizontalflip',
  70. ' flips selection or image horizontally.',
  71. '',
  72. ' -verticalflip',
  73. ' flips selection or image vertically.',
  74. '',
  75. ' -swapredblue',
  76. ' swap red and blue channels.',
  77. '',
  78. ' -smartzoom3',
  79. ' resample the image 3 times bigger with smart detection of bor‐',
  80. ' ders.',
  81. '',
  82. ' -rotatecw',
  83. ' rotates the image clockwise.',
  84. '',
  85. ' -rotateccw',
  86. ' rotates the image counter-clockwise.',
  87. '',
  88. ' -rotate180',
  89. ' rotates the image 180 degrees.');
  90. procedure ProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList; out errorEncountered, fileSaved, quitQuery: boolean);
  91. function ParamStrUTF8(AIndex: integer): string;
  92. implementation
  93. uses
  94. SysUtils, BGRAUTF8, LazFileUtils, BGRABitmap, BGRABitmapTypes, BGRALayers, Dialogs, uparse,
  95. UImage, UImageAction, ULayerAction, UScripting, UPython, Forms, Controls,
  96. UFileSystem, BGRAIconCursor, UGraph
  97. {$IFDEF SHOW_MANUAL_IN_WINDOW},StdCtrls{$ENDIF};
  98. function ParamStrUTF8(AIndex: integer): string;
  99. begin
  100. result := SysToUTF8(ParamStr(AIndex)); //not perfect
  101. end;
  102. procedure InternalProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList;
  103. out errorEncountered, fileSaved, quitQuery: boolean; AImageActions: TImageActions);
  104. var
  105. commandPrefix: set of char;
  106. InputFilename:string;
  107. i,iStart: integer;
  108. errPos: integer; //number conversion
  109. //functions
  110. CommandStr,LowerCmd:string;
  111. funcParams: ArrayOfString;
  112. Filter: TPictureFilter;
  113. //resample
  114. w,h: integer;
  115. //opacity
  116. opacity: byte;
  117. //gradient
  118. c1,c2: TBGRAPixel;
  119. gt: TGradientType;
  120. o1,o2: TPointF;
  121. layerAction: TLayerAction;
  122. enableScript: Boolean;
  123. function DoGradient: boolean;
  124. begin
  125. //c1, c2: TBGRAPixel; gtype: TGradientType; o1, o2: TPointF;
  126. funcParams := SimpleParseFuncParam(CommandStr);
  127. if length(funcParams)<>13 then
  128. begin
  129. instance.ShowError('Gradient','"Gradient" '+StringReplace(rsExpectNParameters,'N','13',[])+'red1,green1,blue1,alpha1,red2,green2,blue2,alpha2,type,x1,y1,x2,y2');
  130. errorEncountered := true;
  131. exit(false);
  132. end;
  133. val(funcParams[0],c1.red,errPos);
  134. val(funcParams[1],c1.green,errPos);
  135. val(funcParams[2],c1.blue,errPos);
  136. val(funcParams[3],c1.alpha,errPos);
  137. val(funcParams[4],c2.red,errPos);
  138. val(funcParams[5],c2.green,errPos);
  139. val(funcParams[6],c2.blue,errPos);
  140. val(funcParams[7],c2.alpha,errPos);
  141. gt := StrToGradientType(funcParams[8]);
  142. val(funcParams[9],o1.x,errPos);
  143. val(funcParams[10],o1.y,errPos);
  144. val(funcParams[11],o2.x,errPos);
  145. val(funcParams[12],o2.y,errPos);
  146. layerAction := instance.Image.CreateAction(true);
  147. layerAction.DrawingLayer.GradientFill(0,0,
  148. instance.Image.Width,instance.Image.Height,
  149. c1,c2,gt,o1,o2,dmDrawWithTransparency,True,False);
  150. layerAction.Validate;
  151. FreeAndNil(layerAction);
  152. result := true;
  153. end;
  154. function DoOpacity: boolean;
  155. begin
  156. funcParams := SimpleParseFuncParam(CommandStr);
  157. if length(funcParams)<>1 then
  158. begin
  159. instance.ShowError('Opacity','"Opacity" ' + rsExpect1Parameter+CommandStr);
  160. errorEncountered := true;
  161. exit(false);
  162. end;
  163. val(funcParams[0],opacity,errPos);
  164. if (errPos <> 0) then
  165. begin
  166. instance.ShowError('Opacity',rsInvalidOpacity+CommandStr);
  167. errorEncountered := true;
  168. exit(false);
  169. end;
  170. layerAction := instance.Image.CreateAction(true);
  171. layerAction.DrawingLayer.ApplyGlobalOpacity(opacity);
  172. layerAction.Validate;
  173. FreeAndNil(layerAction);
  174. result := true;
  175. end;
  176. function DoResample: boolean;
  177. begin
  178. funcParams := SimpleParseFuncParam(CommandStr);
  179. if length(funcParams)<>2 then
  180. begin
  181. instance.ShowError('Resample','"Resample" ' + rsExpect2Parameters+CommandStr);
  182. errorEncountered := true;
  183. exit(false);
  184. end;
  185. val(funcParams[0],w,errPos);
  186. val(funcParams[1],h,errPos);
  187. if (errPos <> 0) or (w <= 0) or (h <= 0) then
  188. begin
  189. instance.ShowError('Resample',rsInvalidResampleSize+CommandStr);
  190. errorEncountered := true;
  191. exit(false);
  192. end;
  193. instance.Image.Resample(w,h,rfHalfCosine);
  194. result := true;
  195. end;
  196. function DoNew: boolean;
  197. begin
  198. funcParams := SimpleParseFuncParam(CommandStr);
  199. if length(funcParams)<>2 then
  200. begin
  201. instance.ShowError('New','"New" ' + rsExpect2Parameters+CommandStr);
  202. errorEncountered := true;
  203. exit(false);
  204. end;
  205. val(funcParams[0],w,errPos);
  206. val(funcParams[1],h,errPos);
  207. if (errPos <> 0) or (w <= 0) or (h <= 0) then
  208. begin
  209. instance.ShowError('New',rsInvalidSizeForNew+CommandStr);
  210. errorEncountered := true;
  211. exit(false);
  212. end;
  213. instance.Image.Assign(instance.MakeNewBitmapReplacement(w,h,BGRAPixelTransparent),True,False);
  214. result := true;
  215. end;
  216. function DoScreenshot: boolean;
  217. var r: TRect;
  218. screenIndex,errPos: integer;
  219. invalid: boolean;
  220. begin
  221. funcParams := SimpleParseFuncParam(CommandStr);
  222. if length(funcParams)=1 then
  223. begin
  224. val(funcParams[0],screenIndex,errPos);
  225. if errPos <> 0 then
  226. begin
  227. instance.ShowError('Screenshot', '"Screenshot" ' + rsExpect1Parameter+CommandStr);
  228. errorEncountered := true;
  229. exit(false);
  230. end;
  231. if (screenIndex < 0) or (screenIndex >= Screen.MonitorCount) then
  232. begin
  233. instance.ShowError('Screenshot', '"Screenshot" ' +
  234. lclstrconsts.rsListIndexExceedsBounds.Replace('%d', inttostr(screenIndex)));
  235. errorEncountered := true;
  236. exit(false);
  237. end;
  238. r := Screen.Monitors[screenIndex].BoundsRect;
  239. r := rect(r.Left*CanvasScale, r.Top*CanvasScale,
  240. r.Right*CanvasScale, r.Bottom*CanvasScale);
  241. end else
  242. if length(funcParams)=4 then
  243. begin
  244. r := rect(0,0,0,0);
  245. invalid := false;
  246. val(funcParams[0],r.Left,errPos);
  247. if errPos <> 0 then invalid := true;
  248. val(funcParams[1],r.Top,errPos);
  249. if errPos <> 0 then invalid := true;
  250. val(funcParams[2],r.Right,errPos);
  251. if errPos <> 0 then invalid := true;
  252. val(funcParams[3],r.Bottom,errPos);
  253. if errPos <> 0 then invalid := true;
  254. if invalid then
  255. begin
  256. instance.ShowError('Screenshot', '"Screenshot" ' + StringReplace(rsExpectNParameters,'N','4',[])+CommandStr);
  257. errorEncountered := true;
  258. exit(false);
  259. end;
  260. if (errPos <> 0) or (r.Width <= 0) or (r.Height <= 0) then
  261. begin
  262. instance.ShowError('New',rsInvalidSizeForNew+IntToStr(r.Width)+'x'+IntToStr(r.Height));
  263. errorEncountered := true;
  264. exit(false);
  265. end;
  266. end else
  267. begin
  268. instance.ShowError('Screenshot', '"Screenshot" ' + StringReplace(rsExpectNParameters,'N','4',[])+CommandStr);
  269. errorEncountered := true;
  270. exit(false);
  271. end;
  272. AImageActions.TakeScreenshot(r);
  273. result := true;
  274. end;
  275. function MakeConfigFromFuncParam: string;
  276. var
  277. cfg: TStringList;
  278. curSection, newSection, p, param: string;
  279. begin
  280. cfg := TStringList.Create;
  281. curSection := '[General]';
  282. cfg.Add(curSection);
  283. try
  284. for p in funcParams do
  285. begin
  286. param := p;
  287. if param.StartsWith('[') then
  288. begin
  289. if param.IndexOf(']') <> -1 then
  290. begin
  291. newSection := param.Substring(0, param.IndexOf(']')+1);
  292. param := param.Substring(length(newSection));
  293. end else
  294. begin
  295. newSection := param;
  296. param := '';
  297. end;
  298. if newSection <> curSection then
  299. begin
  300. curSection := newSection;
  301. cfg.Add(curSection);
  302. end;
  303. end;
  304. if param<>'' then
  305. begin
  306. if param.IndexOf('=') = -1 then
  307. raise Exception.Create(SParExpected.Replace('%s', '"="'));
  308. cfg.Add(param);
  309. end;
  310. end;
  311. finally
  312. result := cfg.Text;
  313. cfg.Free;
  314. end;
  315. end;
  316. function DoEditor: boolean;
  317. var
  318. iniStream: TStream;
  319. bmp: TBGRALayeredBitmap;
  320. begin
  321. result := false;
  322. funcParams := SimpleParseFuncParam(CommandStr);
  323. if (length(funcParams) = 1) and
  324. ((ExtractFileExt(funcParams[0])='.ini') or (ExtractFileExt(funcParams[0])='.cfg')) then
  325. iniStream := FileManager.CreateFileStream(funcParams[0], fmOpenRead)
  326. else if (length(funcParams) = 1) and (funcParams[0] = 'default') then
  327. iniStream := TMemoryStream.Create
  328. else
  329. iniStream := TStringStream.Create(MakeConfigFromFuncParam);
  330. bmp := instance.Image.CurrentState.GetLayeredBitmapCopy;
  331. try
  332. if instance.EditBitmap(bmp, iniStream) then
  333. begin
  334. instance.Image.CurrentState.Assign(bmp, true);
  335. result := true;
  336. end
  337. else
  338. begin
  339. bmp.Free;
  340. quitQuery := true;
  341. end;
  342. finally
  343. FileManager.CancelStreamAndFree(iniStream);
  344. end;
  345. end;
  346. function NextAsFuncParam: boolean;
  347. begin
  348. inc(i);
  349. CommandStr := commandsUTF8[i];
  350. if (length(CommandStr) >= 1) and (CommandStr[1] in commandPrefix) then
  351. begin
  352. instance.ShowError('Command line','Expecting parameters but command found');
  353. exit(false);
  354. end;
  355. result := true;
  356. end;
  357. procedure DisplayHelp;
  358. var
  359. j: Integer;
  360. {$IFDEF SHOW_MANUAL_IN_WINDOW}
  361. f: TForm;
  362. memo: TMemo;
  363. {$ENDIF}
  364. begin
  365. {$IFDEF SHOW_MANUAL_IN_WINDOW}
  366. f := TForm.Create(nil);
  367. try
  368. f.Caption := rsLazPaint;
  369. f.Position:= poDesktopCenter;
  370. f.Width := Screen.Width*3 div 4;
  371. f.Height := Screen.Height*3 div 4;
  372. memo := TMemo.Create(f);
  373. memo.Align:= alClient;
  374. memo.Parent := f;
  375. memo.Font.Name:= 'monospace';
  376. memo.ScrollBars := ssVertical;
  377. memo.Lines.Clear;
  378. for j := low(manual) to high(manual) do
  379. memo.Lines.Add(manual[j]);
  380. f.ShowModal;
  381. finally
  382. f.Free;
  383. end;
  384. {$ELSE}
  385. for j := low(manual) to high(manual) do
  386. writeln(manual[j]);
  387. {$ENDIF}
  388. end;
  389. procedure DoSaveFile(outputFilename: string);
  390. var
  391. icoCur: TBGRAIconCursor;
  392. stream: TStream;
  393. ext: String;
  394. begin
  395. instance.StartSavingImage(outputFilename);
  396. try
  397. ext := ExtractFileExt(outputFilename);
  398. // normally ICO and CUR cannot be saved directly but make an exception
  399. if (CompareText(ext, '.ico')=0) or (CompareText(ext, '.cur')=0) then
  400. begin
  401. icoCur := TBGRAIconCursor.Create;
  402. try
  403. if CompareText(ext, '.cur') = 0 then
  404. icoCur.FileType := ifCur
  405. else icoCur.FileType := ifIco;
  406. icoCur.Add(instance.Image.RenderedImage, BGRABitDepthIconCursor(instance.Image.RenderedImage));
  407. stream := FileManager.CreateFileStream(outputFilename, fmCreate);
  408. try
  409. icoCur.SaveToStream(stream);
  410. finally
  411. stream.Free;
  412. end;
  413. finally
  414. icoCur.Free;
  415. end;
  416. end else
  417. instance.Image.SaveToFileUTF8(outputFilename)
  418. except
  419. on ex: Exception do
  420. begin
  421. instance.ShowError(rsSave, rsUnableToSaveFile+outputFilename);
  422. end;
  423. end;
  424. instance.EndSavingImage;
  425. end;
  426. begin
  427. fileSaved := True;
  428. quitQuery:= false;
  429. errorEncountered := false;
  430. enableScript:= false;
  431. if commandsUTF8.count = 0 then exit;
  432. commandPrefix := ['-'];
  433. {$WARNINGS OFF}
  434. if PathDelim<>'/' then commandPrefix += ['/'];
  435. {$WARNINGS ON}
  436. InputFilename:= commandsUTF8[0];
  437. iStart := 0;
  438. if InputFilename <> '' then
  439. begin
  440. if not (InputFilename[1] in commandPrefix) then
  441. begin
  442. iStart := 1;
  443. if not FileManager.FileExists(ExpandFileNameUTF8(InputFilename)) then
  444. begin
  445. instance.ShowError(rsOpen, rsFileNotFound);
  446. errorEncountered := true;
  447. exit;
  448. end else
  449. if not instance.TryOpenFileUTF8(ExpandFileNameUTF8(InputFilename), true) then
  450. begin
  451. instance.ShowError(rsOpen, rsUnableToLoadFile+InputFilename);
  452. errorEncountered := true;
  453. exit;
  454. end;
  455. end;
  456. end;
  457. fileSaved := false;
  458. i := iStart-1;
  459. while i < commandsUTF8.count-1 do
  460. begin
  461. inc(i);
  462. CommandStr := commandsUTF8[i];
  463. if (length(CommandStr) >= 1) and (CommandStr[1] in commandPrefix) then
  464. begin
  465. if (commandStr[1] = '-') and (length(commandStr)>=2) and (commandStr[2] = '-') then
  466. delete(commandStr,1,2)
  467. else Delete(CommandStr,1,1);
  468. Filter := StrToPictureFilter(CommandStr);
  469. if Filter <> pfNone then
  470. begin
  471. if instance.ExecuteFilter(Filter,True) <> srOk then
  472. begin
  473. instance.ShowError(CommandStr, rsUnableToApplyFilter+CommandStr);
  474. errorEncountered := true;
  475. exit;
  476. end;
  477. end else
  478. begin
  479. LowerCmd := UTF8LowerCase(CommandStr);
  480. if (LowerCmd='help') or (LowerCmd = 'h') or (LowerCmd = '?') then
  481. begin DisplayHelp; quitQuery := true; exit; end else
  482. if LowerCmd='horizontalflip' then AImageActions.HorizontalFlip(foAuto) else
  483. if LowerCmd='verticalflip' then AImageActions.VerticalFlip(foAuto) else
  484. if LowerCmd='swapredblue' then instance.Image.SwapRedBlue else
  485. if LowerCmd='smartzoom3' then AImageActions.SmartZoom3 else
  486. if LowerCmd='rotatecw' then AImageActions.RotateCW else
  487. if LowerCmd='rotateccw' then AImageActions.RotateCCW else
  488. if LowerCmd='rotate180' then AImageActions.Rotate180 else
  489. if copy(lowerCmd,1,9)='gradient(' then begin if not DoGradient then exit end else
  490. if lowerCmd = 'gradient' then begin if not NextAsFuncParam or not DoGradient then exit end else
  491. if copy(lowerCmd,1,8)='opacity(' then begin if not DoOpacity then exit end else
  492. if lowerCmd = 'opacity' then begin if not NextAsFuncParam or not DoOpacity then exit end else
  493. if copy(lowerCmd,1,9)='resample(' then begin if not DoResample then exit end else
  494. if lowerCmd = 'resample' then begin if not NextAsFuncParam or not DoResample then exit end else
  495. if copy(lowerCmd,1,4)='new(' then begin if not DoNew then exit end else
  496. if lowerCmd = 'new' then begin if not NextAsFuncParam or not DoNew then exit end else
  497. if lowerCmd.StartsWith('screenshot(') then begin if not DoScreenShot then exit end else
  498. if lowerCmd = 'screenshot' then begin if not NextAsFuncParam or not DoScreenShot then exit end else
  499. if lowerCmd.StartsWith('editor(') then begin if not DoEditor then exit end else
  500. if lowerCmd = 'editor' then begin if not NextAsFuncParam or not DoEditor then exit end else
  501. if lowerCmd = 'script' then
  502. begin
  503. enableScript := true;
  504. end else
  505. if lowerCmd = 'scriptbasedir' then
  506. begin
  507. if not NextAsFuncParam then exit;
  508. CustomScriptDirectory:= ChompPathDelim(ExpandFileNameUTF8(commandStr));
  509. end else
  510. if lowerCmd = 'quit' then
  511. begin
  512. quitQuery:= true;
  513. exit;
  514. end else
  515. if Copy(CommandStr,1,4) <> 'psn_' then //ignore mac parameter
  516. begin
  517. instance.ShowError('Command line', rsUnknownCommand+CommandStr);
  518. errorEncountered := true;
  519. exit;
  520. end;
  521. end;
  522. end else
  523. if enableScript then
  524. begin
  525. ForcePathDelims(commandStr);
  526. if (CompareText(ExtractFileExt(CommandStr), '.py') <> 0) then
  527. begin
  528. instance.ShowError('Command line', rsFileExtensionNotSupported + ' ('+ExtractFileExt(commandStr)+')');
  529. errorEncountered:= true;
  530. exit;
  531. end;
  532. if not FileExistsUTF8(commandStr) and
  533. (pos(PathDelim, commandStr) = 0) then
  534. commandStr := TPythonScript.DefaultScriptDirectory + PathDelim + commandStr;
  535. if not instance.RunScript(commandStr) then exit;
  536. enableScript := false;
  537. end else
  538. begin
  539. ForcePathDelims(CommandStr);
  540. DoSaveFile(commandStr);
  541. fileSaved:= true;
  542. exit;
  543. end;
  544. end;
  545. end;
  546. procedure ProcessCommands(instance: TLazPaintCustomInstance; commandsUTF8: TStringList;
  547. out errorEncountered, fileSaved, quitQuery: boolean);
  548. begin
  549. InternalProcessCommands(instance, commandsUTF8, errorEncountered, fileSaved, quitQuery, TImageActions(instance.ImageAction));
  550. end;
  551. end.