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     immutable simpleImageWidth  = 32;
60     immutable 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 && renderer.type == RenderType.opengl)
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 && renderer.type == RenderType.opengl)
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 unittest
496 {
497     Image image = new Image(32, 32);
498 
499     assert(image.bytes!(PixelFormat.RGBA)
500         .validateImageData!(PixelFormat.RGBA)(image.width, image.height));
501 }
502 
503 /++
504 Rotate the picture by the specified angle from the specified center.
505 
506 Params:
507     image = The picture to be rotated.
508     angle = Angle of rotation.
509     center =    Center of rotation.
510                 (If the vector is empty (non-zero `vecfNan`), then the
511                 center is determined automatically).
512 +/
513 Image rotateImage(int Type = WithoutParallel)(Image image, float angle, Vecf center = vecfNaN) @safe
514 {
515     import tida.angle;
516     import std.conv : to;
517 
518     Image rotated = new Image(image.width, image.height);
519     rotated.fill(rgba(0, 0, 0, 0));
520 
521     if (center.isVecfNaN)
522         center = Vecf(image.width / 2, image.height / 2);
523 
524     foreach (x, y; Coord!Type(image.width, image.height))
525     {
526         auto pos = Vecf(x,y)
527             .rotate(angle, center);
528 
529         auto colorpos = image.getPixel(x,y);
530 
531         rotated.setPixel(pos.x.to!int, pos.y.to!int, colorpos);
532     }
533 
534     return rotated;
535 }
536 
537 enum XAxis = 0; /// X-Axis operation.
538 enum YAxis = 1; /// Y-Axis operation.
539 
540 /++
541 Checks the axis for correctness.
542 +/
543 template isValidAxis(int axistype)
544 {
545     enum isValidAxis = axistype == XAxis || axistype == YAxis;
546 }
547 
548 /++
549 Reverses the picture along the specified axis.
550 
551 Params:
552     flipType = Axis.
553     img = Image.
554 
555 Example:
556 ---
557 image
558     .flip!XAxis
559     .flip!YAxis;
560 ---
561 +/
562 Image flip(int flipType)(Image img) @safe
563 in(isValidAxis!flipType)
564 do
565 {
566     Image image = img.dup();
567 
568     static if (flipType == XAxis) {
569         foreach(x,y; Coord(img.width, img.height)) {
570             image.setPixel(img.width - x, y, img.getPixel(x, y));
571         }
572     }else
573     static if (flipType == YAxis)
574     {
575         foreach(x,y; Coord(img.width, img.height)) {
576             image.setPixel(x, img.height - y, img.getPixel(x, y));
577         }
578     }
579 
580     return image;
581 }
582 
583 /++
584 Save image in file.
585 
586 Params:
587     image = Image.
588     path = Path to the file.
589 +/
590 void saveImageInFile(Image image, string path) @trusted
591 in(!image.empty, "The image can not be empty!")
592 do
593 {
594     import imagefmt;
595     import tida.color;
596 
597     write_image(path, image.width, image.height, image.bytes!(PixelFormat.RGBA), 4);
598 }
599 
600 /++
601 Divides the picture into frames.
602 
603 Params:
604     image = Atlas.
605     x = Begin x-axis position divide.
606     y = Begin y-axis position divide.
607     w = Frame width.
608     h = Frame height.
609 +/
610 Image[] strip(Image image, int x, int y, int w, int h) @safe
611 {
612     Image[] result;
613 
614     for (int i = 0; i < image.width; i += w)
615     {
616         result ~= image.copy(i,y,w,h);
617     }
618 
619     return result;
620 }
621 
622 /++
623 Combining two paintings into one using color mixing.
624 
625 Params:
626     a = First image.
627     b = Second image.
628     posA = First image position.
629     posB = Second image position.
630 +/
631 Image unite(int Type = WithoutParallel)(Image a,
632                                         Image b,
633                                         Vecf posA = vecf(0, 0),
634                                         Vecf posB = vecf(0, 0)) @trusted
635 {
636     import std.conv : to;
637     import tida.color;
638 
639     Image result = new Image();
640 
641     int width = int.init,
642         height = int.init;
643 
644     width = (posA.x + a.width > posB.x + b.width) ? posA.x.to!int + a.width : posB.x.to!int + b.width;
645     height = (posA.y + a.height > posB.y + b.height) ? posA.y.to!int + a.height : posB.x.to!int + b.height;
646 
647     result.allocatePlace(width, height);
648     result.fill(rgba(0, 0, 0, 0));
649 
650     foreach (x, y; Coord!Type(  posA.x.to!int + a.width, posA.y.to!int + a.height,
651                                 posA.x.to!int, posA.y.to!int))
652     {
653         Color!ubyte color = a.getPixel(x - posA.x.to!int, y - posA.y.to!int);
654         result.setPixel(x,y,color);
655     }
656 
657     foreach (x, y; Coord!Type(  posB.x.to!int + b.width, posB.y.to!int + b.height,
658                                 posB.x.to!int, posB.y.to!int))
659     {
660         Color!ubyte color = b.getPixel(x - posB.x.to!int, y - posB.y.to!int);
661         Color!ubyte backColor = result.getPixel(x, y);
662         color = color.BlendAlpha!ubyte(backColor);
663         result.setPixel(x, y, color);
664     }
665 
666     return result;
667 }
668 
669 /++
670 Generates a gauss matrix.
671 
672 Params:
673     width = Matrix width.
674     height = Matrix height.
675     sigma = Radious gaus.
676 
677 Return: Matrix
678 +/
679 float[][] gausKernel(int width, int height, float sigma) @safe nothrow pure
680 {
681     import std.math : exp, PI;
682 
683     float[][] result = new float[][](width, height);
684 
685     float sum = 0f;
686 
687     foreach (i; 0 .. height)
688     {
689         foreach (j; 0 .. width)
690         {
691             result[j][i] = exp(-(i * i + j * j) / (2 * sigma * sigma) / (2 * PI * sigma * sigma));
692             sum += result[j][i];
693         }
694     }
695 
696     foreach (i; 0 .. height)
697     {
698         foreach (j; 0 .. width)
699         {
700             result[j][i] /= sum;
701         }
702     }
703 
704     return result;
705 }
706 
707 /++
708 Generates a gauss matrix.
709 
710 Params:
711     r = Radiuos gaus.
712 +/
713 float[][] gausKernel(float r) @safe nothrow pure
714 {
715     return gausKernel(cast(int) (r * 2), cast(int) (r * 2), r);
716 }
717 
718 /++
719 Applies blur.
720 
721 Params:
722     Type = Operation type.
723     image = Image.
724     r = radius gaus kernel.
725 +/
726 Image blur(int Type = WithoutParallel)(Image image, float r) @safe
727 {
728     return blur!Type(image, gausKernel(r));
729 }
730 
731 /++
732 Apolies blur.
733 
734 Params:
735     Type = Operation type.
736     image = Image.
737     width = gaus kernel width.
738     height = gaus kernel height.
739     r = radius gaus kernel.
740 +/
741 Image blur(int Type = WithoutParallel)(Image image, int width, int height, float r) @safe
742 {
743     return blur!Type(image, gausKernel(width, height, r));
744 }
745 
746 /++
747 Applies blur.
748 
749 Params:
750     Type = Operation type.
751     image = Image.
752     otherKernel = Filter kernel.
753 +/
754 Image blur(int Type = WithoutParallel)(Image image, float[][] otherKernel) @trusted
755 {
756     import tida.color;
757 
758     auto kernel = otherKernel; 
759     
760     int width = image.width;
761     int height = image.height;
762 
763     int kernelWidth = cast(int) kernel.length;
764     int kernelHeight = cast(int) kernel[0].length;
765 
766     Image result = new Image(width,height);
767     result.fill(rgba(0,0,0,0));
768 
769     foreach (x, y; Coord!Type(width, height))
770     {
771         Color!ubyte color = rgb(0,0,0);
772 
773         foreach (ix,iy; Coord(kernelWidth, kernelHeight))
774         {
775             color = color + (image.getPixel(x - kernelWidth / 2 + ix,y - kernelHeight / 2 + iy) * (kernel[ix][iy]));
776         }
777 
778         color.a = image.getPixel(x,y).a;
779         result.setPixel(x,y,color);
780     }
781 
782     return result;
783 }
784 
785 import tida.color : Color; 
786 import tida.vector : Vector, Vecf, vecf;
787 
788 /++
789 Image processing process. The function traverses the picture, giving the
790 input delegate a pointer to the color and traversal position in the form
791 of a vector.
792 
793 Params:
794     image = Image processing.
795     func = Function processing.
796 
797 Example:
798 ---
799 // Darkening the picture in the corners.
800 myImage.process((ref e, position) {
801     e = e * (position.x / myImage.width * position.y / myImage.height);
802 });
803 ---
804 +/
805 Image process(int Type = WithoutParallel)(  Image image, 
806                                             void delegate(  ref Color!ubyte, 
807                                                             const Vecf) @safe func) @safe
808 {
809     foreach(x, y; Coord!Type(image.width, image.height)) 
810     {
811         Color!ubyte color = image.getPixel(x, y);
812         func(color, vecf(x, y));
813         image.setPixel(x, y, color);
814     }
815 
816     return image;
817 }
818 
819 unittest
820 {
821     import tida.color, tida.vector;
822 
823     Image image = new Image(64, 64);
824     image.fill(rgba(128, 128, 128, 255));
825 
826     image.process((ref e, position) {
827         e = rgb(64, 64, 64);
828     });
829 
830     assert(image.getPixel(32, 32) == (rgb(64, 64, 64)));
831 }