1 /++
2 A module for manipulating pictures, processing and transforming them.
3 
4 Using the $(HREF https://code.dlang.org/packages/imagefmt, imagefmt) library,
5 you can load images directly from files with a certain limitation in types
6 (see imagefmt). Also, the image can be manipulated: $(LREF blur),
7 $(HREF image/Image.copy.html, copy), $(HREF #process, apply loop processing).
8 
9 Blur can be applied simply by calling the function with the picture as an
10 argument. At the exit, it will render the image with blur. Example:
11 ---
12 Image image = new Image().load("test.png");
13 image = image.blur!WithParallel(4);
14 // `WithParallel` means that parallel computation is used
15 // (more precisely, application, the generation of a Gaussian kernel
16 // will be without it).
17 ---
18 
19 Also, you can set a custom kernel to use, let's say:
20 ---
21 // Generating a gaussian kernel. In fact, you can make your own kernel.
22 auto kernel = gausKernel(4, 4, 4);
23 
24 image = image.blur!WithParallel(kernel);
25 ---
26 
27 Macros:
28     LREF = <a href="#$1">$1</a>
29     HREF = <a href="$1">$2</a>
30 
31 Authors: $(HREF https://github.com/TodNaz,TodNaz)
32 Copyright: Copyright (c) 2020 - 2021, TodNaz.
33 License: $(HREF https://github.com/TodNaz/Tida/blob/master/LICENSE,MIT)
34 +/
35 module tida.image;
36 
37 import tida.color;
38 import tida.each;
39 import tida.drawable;
40 import tida.vector;
41 
42 /++
43 Checks if the data for the image is valid.
44 
45 Params:
46     format = Pixel format.
47     data = Image data (in fact, it only learns the length from it).
48     w = Image width.
49     h = Image height.
50 +/
51 bool validateImageData(int format)(ubyte[] data, uint w, uint h) @safe nothrow pure
52 {
53     return data.length == (w * h * bytesPerColor!format);
54 }
55 
56 /// Let's check the performance:
57 unittest
58 {
59     uint simpleImageWidth = 32;
60     uint simpleImageHeight = 32;
61     ubyte[] data = new ubyte[](simpleImageWidth * simpleImageHeight * bytesPerColor!(PixelFormat.RGBA));
62 
63     assert(validateImageData!(PixelFormat.RGBA)(data, simpleImageWidth, simpleImageHeight));
64 }
65 
66 /++
67 Image description structure. (Colors are in RGBA format.)
68 +/
69 class Image : IDrawable, IDrawableEx
70 {
71     import std.algorithm : fill;
72     import tida.vector;
73     import tida.render;
74     import tida.texture;
75 
76 private:
77     Color!ubyte[] pixeldata;
78     uint _width;
79     uint _height;
80     Texture _texture;
81 
82 public:
83     /++
84     Loads a surface from a file. Supported formats are described here:
85     `https://code.dlang.org/packages/imageformats`
86 
87     Params:
88         path = Relative or full path to the image file.
89     +/
90     Image load(string path) @trusted
91     {
92         import imagefmt;
93         import std.file : exists;
94         import std.exception : enforce;
95 
96         enforce!Exception(exists(path), "Not find file `"~path~"`!");
97 
98         IFImage temp = read_image(path, 4);
99         scope(exit) temp.free();
100 
101         _width = temp.w;
102         _height = temp.h;
103 
104         allocatePlace(_width, _height);
105 
106         bytes!(PixelFormat.RGBA)(temp.buf8);
107 
108         return this;
109     }
110 
111     /++
112     Merges two images (no alpha blending).
113 
114     Params:
115         otherImage = Other image for merge.
116         position = Image place position.
117     +/
118     void blit(int Type = WithoutParallel)(Image otherImage, Vecf position) @trusted
119     {
120         import std.conv : to;
121 
122         foreach (x, y; Coord!Type(  position.x.to!int + otherImage.width, 
123                                     position.y.to!int+ otherImage.height,
124                                     position.x.to!int, position.y.to!int))
125         {
126             this.setPixel(x, y, otherImage.getPixel(x - position.x.to!int,
127                                                     y - position.y.to!int));
128         }
129     }
130 
131     /++
132     Redraws the part to a new picture.
133     
134     Params:
135         x = The x-axis position.
136         y = The y-axis position.
137         cWidth = Width new picture.
138         cHeight = Hight new picture.
139     +/
140     Image copy(int Type = WithoutParallel)(int x, int y, int cWidth, int cHeight) @trusted
141     {
142         Image image = new Image();
143         image.allocatePlace(cWidth,cHeight);
144 
145         foreach (ix, iy; Coord!Type(x + cWidth, y + cHeight, x, y))
146         {
147             image.setPixel(
148                 ix - x, iy - y,
149                 this.getPixel(ix, iy)
150             );
151         }
152 
153         return image;
154     }
155 
156     /++
157     Converts an image to a texture (i.e. for rendering an image with hardware
158     acceleration). At the same time, now, when the image is called, the texture
159     itself will be drawn. You can get such a texture and change its parameters
160     from the $(LREF "texture") property.
161     +/
162     void toTexture() @trusted
163     {
164         import tida.shape;
165         import tida.vertgen;
166 
167         _texture = new Texture();
168 
169         Shape!float shape = Shape!float.Rectangle(vecf(0, 0), vecf(width, height));
170 
171         TextureInfo info;
172         info.width = _width;
173         info.height = _height;
174         info.params = DefaultParams;
175         info.data = bytes!(PixelFormat.RGBA)();
176 
177         _texture.initializeFromData!(PixelFormat.RGBA)(info);
178         _texture.vertexInfo = generateVertex!(float)(shape, vecf(_width, _height));
179     }
180 
181     /++
182     Converts an image to a texture (i.e. for rendering an image with hardware
183     acceleration). At the same time, now, when the image is called, the texture
184     itself will be drawn. You can get such a texture and change its parameters
185     from the $(LREF "texture") property.
186     +/
187     void toTextureWithoutShape() @trusted
188     {
189         import tida.shape;
190         import tida.vertgen;
191 
192         _texture = new Texture();
193 
194         Shape!float shape = Shape!float.Rectangle(vecf(0, 0), vecf(width, height));
195 
196         TextureInfo info;
197         info.width = _width;
198         info.height = _height;
199         info.params = DefaultParams;
200         info.data = bytes!(PixelFormat.RGBA)();
201 
202         _texture.initializeFromData!(PixelFormat.RGBA)(info);
203     }
204 
205     /++
206     The texture that was generated by the $(LREF "toTexture") function.
207     Using the field, you can set the shape of the texture and its other parameters.
208     +/
209     @property Texture texture() nothrow pure @safe
210     {
211         return _texture;
212     }
213 
214     override void draw(IRenderer renderer, Vecf position) @trusted
215     {
216         import std.conv : to;
217 
218         if (_texture !is null)
219         {
220             _texture.draw(renderer, position);
221             return;
222         }
223 
224         foreach (x; position.x .. position.x + _width)
225         {
226             foreach (y; position.y .. position.y + _height)
227             {
228                 renderer.point(vecf(x, y),
229                                 getPixel(x.to!int - position.x.to!int,
230                                         y.to!int - position.y.to!int));
231             }
232         }
233     }
234 
235     override void drawEx(   IRenderer renderer,
236                             Vecf position,
237                             float angle,
238                             Vecf center,
239                             Vecf size,
240                             ubyte alpha,
241                             Color!ubyte color) @trusted
242     {
243         import std.conv : to;
244         import tida.angle : rotate;
245 
246         if (texture !is null)
247         {
248             texture.drawEx(renderer, position, angle, center, size, alpha, color);
249             return;
250         }
251 
252         if (!size.isVecfNaN)
253         {
254             Image scaled = this.dup.resize(size.x.to!int, size.y.to!int);
255             scaled.drawEx(renderer, position, angle, center, vecfNaN, alpha, color);
256             return;
257         }
258 
259         if (center.isVecfNaN)
260             center = vecf(width, height) / 2;
261 
262         center += position;
263 
264         foreach (x; position.x .. position.x + _width)
265         {
266             foreach (y; position.y .. position.y + _height)
267             {
268                 Vecf pos = vecf(x,y);
269                 pos = rotate(pos, angle, center);
270 
271                 renderer.point(pos,
272                                 getPixel(x.to!int - position.x.to!int,
273                                          y.to!int - position.y.to!int));
274             }
275         }
276     }
277 
278 @safe nothrow pure:
279     /// Empty constructor image.
280     this() @safe
281     {
282         pixeldata = null;
283     }
284 
285     /++
286     Allocates space for image data.
287 
288     Params:
289         w = Image width.
290         h = Image heght.
291     +/
292     this(uint w, uint h)
293     {
294         this.allocatePlace(w, h);
295     }
296 
297     /// Image width.
298     @property uint width()
299     {
300         return _width;
301     }
302 
303     /// Image height.
304     @property uint height()
305     {
306         return _height;
307     }
308 
309     /++
310     Allocates space for image data.
311 
312     Params:
313         w = Image width.
314         h = Image heght.
315     +/
316     void allocatePlace(uint w, uint h)
317     {
318         pixeldata = new Color!ubyte[](w * h);
319 
320         _width = w;
321         _height = h;
322     }
323 
324     /// Image dataset.
325     @property Color!ubyte[] pixels()
326     {
327         return pixeldata;
328     } 
329 
330     /// ditto
331     @property void pixels(Color!ubyte[] data)
332     in(pixeldata.length == (_width * _height))
333     do
334     {
335         pixeldata = data;
336     }
337 
338     /++
339     Gives away bytes as data for an image.
340     +/
341     ubyte[] bytes(int format = PixelFormat.RGBA)()
342     {
343         static assert(isValidFormat!format, CannotDetectAuto);
344 
345         ubyte[] data;
346 
347         foreach (e; pixels)
348             data ~= e.toBytes!format;
349 
350         return data;
351     }
352 
353     /++
354     Accepts bytes as data for an image.
355 
356     Params:
357         data = Image data.
358     +/
359     void bytes(int format = PixelFormat.RGBA)(ubyte[] data)
360     in(validateBytes!format(data))
361     in(validateImageData!format(data, _width, _height))
362     do
363     {
364         pixels = data.fromColors!(format);
365     }
366 
367     /++
368     Whether the data has the current image.
369     +/
370     bool empty()
371     {
372         return pixeldata.length == 0;
373     }
374 
375     /++
376     Fills the data with color.
377 
378     Params:
379         color = The color that will fill the plane of the picture.
380     +/
381     void fill(Color!ubyte color)
382     {
383         pixeldata.fill(color);
384     }
385 
386     /++
387     Sets the pixel to the specified position.
388 
389     Params:
390         x = Pixel x-axis position.
391         y = Pixel y-axis position.
392         color = Pixel color.
393     +/
394     void setPixel(int x, int y, Color!ubyte color)
395     {
396         if (x >= width || y >= height || x < 0 || y < 0)
397             return;
398 
399         pixeldata   [
400                         (width * y) + x
401                     ] = color;  
402     }
403 
404     /++
405     Returns the pixel at the specified location.
406 
407     Params:
408         x = The x-axis position pixel.
409         y = The y-axis position pixel.
410     +/
411     Color!ubyte getPixel(int x,int y)
412     {
413         if (x >= width || y >= height || x < 0 || y < 0) 
414             return Color!ubyte(0, 0, 0, 0);
415 
416         return pixeldata[(width * y) + x];
417     }
418 
419     /++
420     Resizes the image.
421 
422     Params:
423         newWidth = Image new width.
424         newHeight = Image new height.
425     +/
426     Image resize(uint newWidth,uint newHeight)
427     {
428         uint oldWidth = _width;
429         uint oldHeight = _height;
430 
431         _width = newWidth;
432         _height = newHeight;
433 
434         double scaleWidth = cast(double) newWidth / cast(double) oldWidth;
435         double scaleHeight = cast(double) newHeight / cast(double) oldHeight;
436 
437         Color!ubyte[] npixels = new Color!ubyte[](newWidth * newHeight);
438 
439         foreach (cy; 0 .. newHeight)
440         {
441             foreach (cx; 0 .. newWidth)
442             {
443                 uint pixel = (cy * (newWidth)) + (cx);
444                 uint nearestMatch = (cast(uint) (cy / scaleHeight)) * (oldWidth) + (cast(uint) ((cx / scaleWidth)));
445 
446                 npixels[pixel] = pixeldata[nearestMatch];
447             }
448         }
449 
450         pixeldata = npixels;
451 
452         return this;
453     }
454 
455     /++
456     Enlarges the image by a factor.
457 
458     Params:
459         k = factor.
460     +/
461     Image scale(float k)
462     {
463         uint newWidth = cast(uint) ((cast(float) _width) * k);
464         uint newHeight = cast(uint) ((cast(float) _height) * k);
465 
466         return resize(newWidth,newHeight);
467     }
468 
469     /// Dynamic copy of the image.
470     @property Image dup()
471     {
472         Image image = new Image(_width, _height);
473         image.pixels = this.pixels.dup;
474 
475         return image;
476     }
477 
478     /// Free image data.
479     void freeData() @trusted
480     {
481         import core.memory;
482 
483         destroy(pixeldata);
484         pixeldata = null;
485         _width = 0;
486         _height = 0;
487     }
488 
489     ~this()
490     {
491         this.freeData();
492     }
493 }
494 
495 /++
496 Rotate the picture by the specified angle from the specified center.
497 
498 Params:
499     image = The picture to be rotated.
500     angle = Angle of rotation.
501     center =    Center of rotation.
502                 (If the vector is empty (non-zero `vecfNan`), then the
503                 center is determined automatically).
504 +/
505 Image rotateImage(int Type = WithoutParallel)(Image image, float angle, Vecf center = vecfNaN) @safe
506 {
507     import tida.angle;
508     import std.conv : to;
509 
510     Image rotated = new Image(image.width, image.height);
511     rotated.fill(rgba(0, 0, 0, 0));
512 
513     if (center.isVecfNaN)
514         center = Vecf(image.width / 2, image.height / 2);
515 
516     foreach (x, y; Coord!Type(image.width, image.height))
517     {
518         auto pos = Vecf(x,y)
519             .rotate(angle, center);
520 
521         auto colorpos = image.getPixel(x,y);
522 
523         rotated.setPixel(pos.x.to!int, pos.y.to!int, colorpos);
524     }
525 
526     return rotated;
527 }
528 
529 enum XAxis = 0; /// X-Axis operation.
530 enum YAxis = 1; /// Y-Axis operation.
531 
532 /++
533 Checks the axis for correctness.
534 +/
535 template isValidAxis(int axistype)
536 {
537     enum isValidAxis = axistype == XAxis || axistype == YAxis;
538 }
539 
540 /++
541 Reverses the picture along the specified axis.
542 
543 Params:
544     flipType = Axis.
545     img = Image.
546 
547 Example:
548 ---
549 image
550     .flip!XAxis
551     .flip!YAxis;
552 ---
553 +/
554 Image flip(int flipType)(Image img) @safe
555 in(isValidAxis!flipType)
556 do
557 {
558     Image image = img.dup();
559 
560     static if (flipType == XAxis) {
561         foreach(x,y; Coord(img.width, img.height)) {
562             image.setPixel(img.width - x, y, img.getPixel(x, y));
563         }
564     }else
565     static if (flipType == YAxis)
566     {
567         foreach(x,y; Coord(img.width, img.height)) {
568             image.setPixel(x, img.height - y, img.getPixel(x, y));
569         }
570     }
571 
572     return image;
573 }
574 
575 /++
576 Save image in file.
577 
578 Params:
579     image = Image.
580     path = Path to the file.
581 +/
582 void saveImageInFile(Image image, string path) @trusted
583 in(!image.empty, "The image can not be empty!")
584 do
585 {
586     import imagefmt;
587     import tida.color;
588 
589     write_image(path, image.width, image.height, image.bytes!(PixelFormat.RGBA), 4);
590 }
591 
592 /++
593 Divides the picture into frames.
594 
595 Params:
596     image = Atlas.
597     x = Begin x-axis position divide.
598     y = Begin y-axis position divide.
599     w = Frame width.
600     h = Frame height.
601 +/
602 Image[] strip(Image image, int x, int y, int w, int h) @safe
603 {
604     Image[] result;
605 
606     for (int i = 0; i < image.width; i += w)
607     {
608         result ~= image.copy(i,y,w,h);
609     }
610 
611     return result;
612 }
613 
614 /++
615 Combining two paintings into one using color mixing.
616 
617 Params:
618     a = First image.
619     b = Second image.
620     posA = First image position.
621     posB = Second image position.
622 +/
623 Image unite(int Type = WithoutParallel)(Image a,
624                                         Image b,
625                                         Vecf posA = vecf(0, 0),
626                                         Vecf posB = vecf(0, 0)) @trusted
627 {
628     import std.conv : to;
629     import tida.color;
630 
631     Image result = new Image();
632 
633     int width = int.init,
634         height = int.init;
635 
636     width = (posA.x + a.width > posB.x + b.width) ? posA.x.to!int + a.width : posB.x.to!int + b.width;
637     height = (posA.y + a.height > posB.y + b.height) ? posA.y.to!int + a.height : posB.x.to!int + b.height;
638 
639     result.allocatePlace(width, height);
640     result.fill(rgba(0, 0, 0, 0));
641 
642     foreach (x, y; Coord!Type(  posA.x.to!int + a.width, posA.y.to!int + a.height,
643                                 posA.x.to!int, posA.y.to!int))
644     {
645         Color!ubyte color = a.getPixel(x - posA.x.to!int, y - posA.y.to!int);
646         result.setPixel(x,y,color);
647     }
648 
649     foreach (x, y; Coord!Type(  posB.x.to!int + b.width, posB.y.to!int + b.height,
650                                 posB.x.to!int, posB.y.to!int))
651     {
652         Color!ubyte color = b.getPixel(x - posB.x.to!int, y - posB.y.to!int);
653         Color!ubyte backColor = result.getPixel(x, y);
654         color = color.BlendAlpha!ubyte(backColor);
655         result.setPixel(x, y, color);
656     }
657 
658     return result;
659 }
660 
661 /++
662 Generates a gauss matrix.
663 
664 Params:
665     width = Matrix width.
666     height = Matrix height.
667     sigma = Radious gaus.
668 
669 Return: Matrix
670 +/
671 float[][] gausKernel(int width, int height, float sigma) @safe nothrow pure
672 {
673     import std.math : exp, PI;
674 
675     float[][] result = new float[][](width, height);
676 
677     float sum = 0f;
678 
679     foreach (i; 0 .. height)
680     {
681         foreach (j; 0 .. width)
682         {
683             result[j][i] = exp(-(i * i + j * j) / (2 * sigma * sigma) / (2 * PI * sigma * sigma));
684             sum += result[j][i];
685         }
686     }
687 
688     foreach (i; 0 .. height)
689     {
690         foreach (j; 0 .. width)
691         {
692             result[j][i] /= sum;
693         }
694     }
695 
696     return result;
697 }
698 
699 /++
700 Generates a gauss matrix.
701 
702 Params:
703     r = Radiuos gaus.
704 +/
705 float[][] gausKernel(float r) @safe nothrow pure
706 {
707     return gausKernel(cast(int) (r * 2), cast(int) (r * 2), r);
708 }
709 
710 /++
711 Applies blur.
712 
713 Params:
714     Type = Operation type.
715     image = Image.
716     r = radius gaus kernel.
717 +/
718 Image blur(int Type = WithoutParallel)(Image image, float r) @safe
719 {
720     return blur!Type(image, gausKernel(r));
721 }
722 
723 /++
724 Apolies blur.
725 
726 Params:
727     Type = Operation type.
728     image = Image.
729     width = gaus kernel width.
730     height = gaus kernel height.
731     r = radius gaus kernel.
732 +/
733 Image blur(int Type = WithoutParallel)(Image image, int width, int height, float r) @safe
734 {
735     return blur!Type(image, gausKernel(width, height, r));
736 }
737 
738 /++
739 Applies blur.
740 
741 Params:
742     Type = Operation type.
743     image = Image.
744     otherKernel = Filter kernel.
745 +/
746 Image blur(int Type = WithoutParallel)(Image image, float[][] otherKernel) @trusted
747 {
748     import tida.color;
749 
750     auto kernel = otherKernel; 
751     
752     int width = image.width;
753     int height = image.height;
754 
755     int kernelWidth = cast(int) kernel.length;
756     int kernelHeight = cast(int) kernel[0].length;
757 
758     Image result = new Image(width,height);
759     result.fill(rgba(0,0,0,0));
760 
761     foreach (x, y; Coord!Type(width, height))
762     {
763         Color!ubyte color = rgb(0,0,0);
764 
765         foreach (ix,iy; Coord(kernelWidth, kernelHeight))
766         {
767             color = color + (image.getPixel(x - kernelWidth / 2 + ix,y - kernelHeight / 2 + iy) * (kernel[ix][iy]));
768         }
769 
770         color.a = image.getPixel(x,y).a;
771         result.setPixel(x,y,color);
772     }
773 
774     return result;
775 }
776 
777 import tida.color : Color; 
778 import tida.vector : Vector, Vecf, vecf;
779 
780 /++
781 Image processing process. The function traverses the picture, giving the
782 input delegate a pointer to the color and traversal position in the form
783 of a vector.
784 
785 Params:
786     image = Image processing.
787     func = Function processing.
788 
789 Example:
790 ---
791 // Darkening the picture in the corners.
792 myImage.process((ref e, position) {
793     e = e * (position.x / myImage.width * position.y / myImage.height);
794 });
795 ---
796 +/
797 Image process(int Type = WithoutParallel)(  Image image, 
798                                             void delegate(  ref Color!ubyte, 
799                                                             const Vecf) @safe func) @safe
800 {
801     foreach(x, y; Coord!Type(image.width, image.height)) 
802     {
803         Color!ubyte color = image.getPixel(x, y);
804         func(color, vecf(x, y));
805         image.setPixel(x, y, color);
806     }
807 
808     return image;
809 }
810 
811 unittest
812 {
813     import tida.color, tida.vector;
814 
815     Image image = new Image(64, 64);
816     image.fill(rgba(128, 128, 128, 255));
817 
818     image.process((ref e, position) {
819         e = rgb(64, 64, 64);
820     });
821 
822     assert(image.getPixel(32, 32) == rgb(64, 64, 64));
823 }