1 /**
2 * Copyright © The Bot Blog 2019
3 * License: MIT (https://github.com/TheBotBlog/thebotbloglib/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module thebotbloglib.imagemanager;
7 
8 import std.conv : to;
9 import std.string : strip, format;
10 import std.array : join, replace;
11 import std.file : exists, remove, copy;
12 
13 /// Text options.
14 final class TextOptions
15 {
16   public:
17   /// The name of the font to use if no font-file is specified.
18   string fontName;
19   /// The font file to use if no font-name is specified.
20   string fontFile;
21   /// The font size.
22   float fontSize;
23   /// The color of the text.
24   Color color;
25   /// The rectangle to confine the text within.
26   Rectangle rect;
27   /// Boolean determining whether to center the text in the given rectangle.
28   bool centerText;
29 }
30 
31 /// A color
32 final struct Color
33 {
34   public:
35   /// The red channel.
36   ubyte r;
37   /// The green channel.
38   ubyte g;
39   /// The blue channel.
40   ubyte b;
41   /// The alpha channel.
42   ubyte a;
43 
44   static:
45   /**
46   * Creates a color from RGB.
47   * Params:
48   *   r = the red channel.
49   *   g = the green channel.
50   *   b = The blue channel.
51   * Returns:
52   *   The color.
53   */
54   Color rgb(int r, int g, int b)
55   {
56     return Color(cast(ubyte)r, cast(ubyte)g, cast(ubyte)b, cast(ubyte)255);
57   }
58 
59   /**
60   * Creates a color from RGBA.
61   * Params:
62   *   r = the red channel.
63   *   g = the green channel.
64   *   b = The blue channel.
65   *   a = The alpha channel.
66   * Returns:
67   *   The color.
68   */
69   Color rgba(int r, int g, int b, int a)
70   {
71     return Color(cast(ubyte)r, cast(ubyte)g, cast(ubyte)b, cast(ubyte)a);
72   }
73 }
74 
75 /// A rectangle.
76 final class Rectangle
77 {
78   public:
79   /// The x coordinate.
80   ptrdiff_t x;
81   /// The y coordinate.
82   ptrdiff_t y;
83   /// The width.
84   ptrdiff_t width;
85   /// The height.
86   ptrdiff_t height;
87   /// Boolean determining whether the width is fixed and relative to the image width.
88   bool fixedWidth;
89   /// Boolean determining whether the height is fixed and relative to the image height.
90   bool fixedHeight;
91 }
92 
93 /// An image manager.
94 final class ImageManager
95 {
96   private:
97   /// The base image path.
98   string _baseImagePath;
99   /// The final output path.
100   string _finalOutputPath;
101   /// The original output path.
102   string _originalOutputPath;
103   /// The output path.
104   string _outputPath;
105   /// The counter.
106   size_t _counter;
107 
108   public:
109   final:
110   /**
111   * Creates a new image manager.
112   * Params:
113   *   baseImagePath = The base image path.
114   *   outputPath = The output image path. This must contain '%s' as a format in the filename. (The final image will not contain the format.)
115   */
116   this(string baseImagePath, string outputPath)
117   {
118     _counter = 0;
119 
120     _baseImagePath = baseImagePath;
121 
122     _finalOutputPath = outputPath.replace("%s", "");
123     _originalOutputPath = outputPath;
124 
125     _outputPath = _originalOutputPath.format(_counter);
126   }
127 
128   /**
129   * Merges another image on-top.
130   * Returns:
131   *   True if the action was successfully executed, false otherwise.
132   */
133   bool merge(string imagePath)
134   {
135     return runImageCmd("merge", imagePath);
136   }
137 
138   /**
139   * Rotates the image 90 degrees.
140   * Returns:
141   *   True if the action was successfully executed, false otherwise.
142   */
143   bool rotate90()
144   {
145     return runImageCmd("rotate90");
146   }
147 
148   /**
149   * Rotate the image 180 degrees.
150   * Returns:
151   *   True if the action was successfully executed, false otherwise.
152   */
153   bool rotate180()
154   {
155     return runImageCmd("rotate180");
156   }
157 
158   /**
159   * Flips the image horizontal.
160   * Returns:
161   *   True if the action was successfully executed, false otherwise.
162   */
163   bool flipHorizontal()
164   {
165     return runImageCmd("flipH");
166   }
167 
168   /**
169   * Flips the image vertically.
170   * Returns:
171   *   True if the action was successfully executed, false otherwise.
172   */
173   bool flipVertical()
174   {
175     return runImageCmd("flipV");
176   }
177 
178   /**
179   * Inverses the colors.
180   * Returns:
181   *   True if the action was successfully executed, false otherwise.
182   */
183   bool inverseColors()
184   {
185     return runImageCmd("inverse");
186   }
187 
188   /**
189   * Turns the image black and white.
190   * Returns:
191   *   True if the action was successfully executed, false otherwise.
192   */
193   bool turnBlackAndWhite()
194   {
195     return runImageCmd("blackAndWhite");
196   }
197 
198   /**
199   * Draws text on the image.
200   * Params:
201   *   text = The text to draw.
202   *   options = The text drawing options.
203   * Returns:
204   *   True if the action was successfully executed, false otherwise.
205   */
206   bool drawText(string text, TextOptions options)
207   {
208     if (!options || !text || !text.strip.length)
209     {
210       return false;
211     }
212 
213     if ((!options.fontName || !options.fontName.strip.length) && (!options.fontFile || !options.fontFile.strip.length))
214     {
215       return false;
216     }
217 
218     auto actions = ["drawText"];
219     actions ~= "text::" ~ text;
220 
221     if (options.fontName && options.fontName.strip.length)
222     {
223       actions ~= "font::" ~ options.fontName;
224     }
225     else
226     {
227       actions ~= "fontFile::" ~ options.fontFile;
228     }
229 
230     if (options.fontSize >= 1)
231     {
232       actions ~= "fontSize::" ~ to!string(options.fontSize);
233     }
234 
235     actions ~= "color::" ~ join([options.color.r.to!string, options.color.g.to!string, options.color.b.to!string, options.color.a.to!string], ",");
236 
237     if (options.rect)
238     {
239       string width = options.rect.fixedWidth ? "*" : to!string(options.rect.width);
240       string height = options.rect.fixedHeight ? "*" : to!string(options.rect.height);
241 
242       actions ~= "rect::" ~ to!string(options.rect.x) ~ "," ~ to!string(options.rect.y) ~ "," ~ width ~ "," ~ height;
243     }
244 
245     if (options.centerText)
246     {
247       actions ~= "centerText::true";
248     }
249 
250     auto action = join(actions, "||");
251 
252     import std.stdio : writeln;
253     writeln(action);
254 
255     return runImageCmd(action);
256   }
257 
258   /**
259   * Draws a rectangle.
260   * Params:
261   *   x = The x coordinate.
262   *   y = The y coordinate.
263   *   width = The width.
264   *   height = The height.
265   *   color = The color.
266   *   fill = (optional) (default = false) Boolean determining whether the rectangle's colors should fill.
267   * Returns:
268   *   True if the action was successfully executed, false otherwise.
269   */
270   bool drawRectangle(ptrdiff_t x, ptrdiff_t y, ptrdiff_t width, ptrdiff_t height, Color color, bool fill = false)
271   {
272     auto action = "drawRect||rect::" ~ to!string(x) ~ "," ~ to!string(y) ~ "," ~ to!string(width) ~ "," ~ to!string(height);
273     action ~= "||color::" ~ join([color.r.to!string, color.g.to!string, color.b.to!string, color.a.to!string], ",");
274 
275     if (fill)
276     {
277       action ~= "||fill::true";
278     }
279 
280     return runImageCmd(action);
281   }
282 
283   /**
284   * Draws a line.
285   * Params:
286   *   startX = The start x coordinate.
287   *   startY = The start y coordinate.
288   *   endX = The end x coordinate.
289   *   endY = The end y coordinate.
290   *   color = The color.
291   * Returns:
292   *   True if the action was successfully executed, false otherwise.
293   */
294   bool drawLine(ptrdiff_t startX, ptrdiff_t startY, ptrdiff_t endX, ptrdiff_t endY, Color color)
295   {
296     auto action = "drawLine||args::" ~ to!string(startX) ~ "," ~ to!string(startY) ~ "," ~ to!string(endX) ~ "," ~ to!string(endY);
297     action ~= "||color::" ~ join([color.r.to!string, color.g.to!string, color.b.to!string, color.a.to!string], ",");
298 
299     return runImageCmd(action);
300   }
301 
302   /**
303   * Draws an image.
304   * Params:
305   *   imagePath = The image path.
306   *   x = The x coordinate.
307   *   y = The y coordinate.
308   *   width = The width.
309   *   height = The height.
310   * Returns:
311   *   True if the action was successfully executed, false otherwise.
312   */
313   bool drawImage(string imagePath, ptrdiff_t x, ptrdiff_t y, ptrdiff_t width, ptrdiff_t height)
314   {
315     return drawImage(imagePath, x, y, to!string(width), to!string(height));
316   }
317 
318   /**
319   * Draws an image.
320   * Params:
321   *   imagePath = The image path.
322   *   x = The x coordinate.
323   *   y = The y coordinate.
324   * Returns:
325   *   True if the action was successfully executed, false otherwise.
326   */
327   bool drawImage(string imagePath, ptrdiff_t x, ptrdiff_t y)
328   {
329     return drawImage(imagePath, x, y, "*", "*");
330   }
331 
332   /**
333   * Draws an image with a custom width.
334   * Params:
335   *   imagePath = The image path.
336   *   x = The x coordinate.
337   *   y = The y coordinate.
338   *   width = The width.
339   * Returns:
340   *   True if the action was successfully executed, false otherwise.
341   */
342   bool drawImageStretchedHorizontal(string imagePath, ptrdiff_t x, ptrdiff_t y, ptrdiff_t width)
343   {
344     return drawImage(imagePath, x, y, to!string(width), "*");
345   }
346 
347   /**
348   * Draws an image with a custom height.
349   * Params:
350   *   imagePath = The image path.
351   *   x = The x coordinate.
352   *   y = The y coordinate.
353   *   height = The height.
354   * Returns:
355   *   True if the action was successfully executed, false otherwise.
356   */
357   bool drawImageStretchedVertical(string imagePath, ptrdiff_t x, ptrdiff_t y, ptrdiff_t height)
358   {
359     return drawImage(imagePath, x, y, "*", to!string(height));
360   }
361 
362   /// Finalizes the image. (Removes the last temp image and creates the final output image file.)
363   void finalize()
364   {
365     copy(_baseImagePath, _finalOutputPath);
366 
367     if (exists(_baseImagePath))
368     {
369       remove(_baseImagePath);
370     }
371   }
372 
373   private:
374   /**
375   * Draws an image.
376   * Params:
377   *   imagePath = The image path.
378   *   x = The x coordinate.
379   *   y = The y coordinate.
380   *   width = The width.
381   *   height = The height.
382   * Returns:
383   *   True if the action was successfully executed, false otherwise.
384   */
385   bool drawImage(string imagePath, ptrdiff_t x, ptrdiff_t y, string width, string height)
386   {
387     auto size = width ~ "," ~ height;
388     auto position = to!string(x) ~ "," ~ to!string(y);
389 
390     return runImageCmd("drawImage||size::" ~ size ~ "||position::" ~ position, imagePath);
391   }
392 
393   /**
394   * Runs the ImageCmd with the given parameters.
395   * Params:
396   *   action = The action / parameters to pass.
397   *   sourceImage = (optional) The source image to use.
398   * Returns:
399   *   True if the action was successfully executed, false otherwise.
400   */
401   bool runImageCmd(string action, string sourceImage = null)
402   {
403     import std.process : spawnProcess, wait, Pid;
404     import std.string : strip;
405 
406     Pid imagePid;
407     if (!sourceImage || !sourceImage.strip.length)
408     {
409       imagePid = spawnProcess(["ImageCmd.exe", _baseImagePath, _outputPath, action]);
410     }
411     else
412     {
413       imagePid = spawnProcess(["ImageCmd.exe", _baseImagePath, sourceImage, action, _outputPath]);
414     }
415 
416     auto result = wait(imagePid);
417 
418     if (_counter)
419     {
420       if (exists(_baseImagePath))
421       {
422         remove(_baseImagePath);
423       }
424     }
425 
426     _baseImagePath = _outputPath;
427     _counter++;
428     _outputPath = _originalOutputPath.format(_counter);
429 
430     return result == 0;
431   }
432 }