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 import std.range : isInputRange, isBidirectionalRange, ElementType;
42 import tida.render;
43 
44 /++
45 Checks if the data for the image is valid.
46 
47 Params:
48     format = Pixel format.
49     data = Image data (in fact, it only learns the length from it).
50     w = Image width.
51     h = Image height.
52 +/
53 bool validateImageData(int format)(ubyte[] data, uint w, uint h) @safe nothrow pure
54 {
55     return data.length == (w * h * bytesPerColor!format);
56 }
57 
58 /// Let's check the performance:
59 unittest
60 {
61     immutable simpleImageWidth  = 32;
62     immutable simpleImageHeight = 32;
63     ubyte[] data = new ubyte[](simpleImageWidth * simpleImageHeight * bytesPerColor!(PixelFormat.RGBA));
64 
65     assert(validateImageData!(PixelFormat.RGBA)(data, simpleImageWidth, simpleImageHeight));
66 }
67 
68 auto reversed(Range)(Range range) @trusted nothrow pure
69 if (isBidirectionalRange!Range)
70 {
71     import std.traits : isArray;
72 
73     ElementType!Range[] elements;
74 
75     foreach_reverse (e; range)
76     {
77         elements ~= e;
78     }
79 
80     static if (is(Range == class))
81         return new Range(elements);
82     else
83     static if (isArray!Range)
84         return elements;
85     else
86         static assert(null, "It is unknown how to return the result.");
87 }
88 
89 /++
90 Converts a sequence of colors to a picture from the input range.
91 
92 Params:
93     range = Sequence of colors.
94     width = Image width.
95     height = Image height.
96     
97 Returns:
98     A picture to be converted from a set of colors.
99 +/
100 Image imageFrom(Range)(Range range, uint width, uint height) @trusted nothrow pure
101 if (isInputRange!Range)
102 {
103     import std.range : array;
104 
105     Image image = new Image(width, height);
106     image.pixels = range.array;
107 
108     return image;
109 }
110 
111 /++
112 Image description structure. (Colors are in RGBA format.)
113 +/
114 class Image : IDrawable, IDrawableEx, ITarget
115 {
116     import std.algorithm : fill, map;
117     import std.range : iota, array;
118     import tida.vector;
119     import tida.texture;
120 
121 private:
122     Color!ubyte[] pixeldata;
123     uint _width;
124     uint _height;
125     Texture _texture;
126 
127 public:
128     /++
129     Loads a surface from a file. Supported formats are described here:
130     `https://code.dlang.org/packages/imageformats`
131 
132     Params:
133         path = Relative or full path to the image file.
134     +/
135     Image load(string path) @trusted
136     {
137         import imagefmt;
138         import std.file : exists;
139         import std.exception : enforce;
140 
141         enforce!Exception(exists(path), "Not find file `"~path~"`!");
142 
143         IFImage temp = read_image(path, 4);
144         scope(exit) temp.free();
145 
146         _width = temp.w;
147         _height = temp.h;
148 
149         allocatePlace(_width, _height);
150 
151         bytes!(PixelFormat.RGBA)(temp.buf8);
152 
153         return this;
154     }
155 
156     /++
157     Merges two images (no alpha blending).
158 
159     Params:
160         otherImage = Other image for merge.
161         position = Image place position.
162     +/
163     void blit(int Type = WithoutParallel)(Image otherImage, Vecf position) @trusted
164     {
165         import std.conv : to;
166 
167         foreach (x, y; Coord!Type(  position.x.to!int + otherImage.width, 
168                                     position.y.to!int+ otherImage.height,
169                                     position.x.to!int, position.y.to!int))
170         {
171             this.setPixel(x, y, otherImage.getPixel(x - position.x.to!int,
172                                                     y - position.y.to!int));
173         }
174     }
175 
176     /++
177     Redraws the part to a new picture.
178     
179     Params:
180         x = The x-axis position.
181         y = The y-axis position.
182         cWidth = Width new picture.
183         cHeight = Hight new picture.
184     +/
185     Image copy(int x, int y, int cWidth, int cHeight) @trusted
186     {
187         import std.algorithm : map, joiner;
188 
189         return this.scanlines[y .. y + cHeight]
190             .map!(e => e[x .. x + cWidth])
191             .joiner
192             .imageFrom(cWidth, cHeight);
193     }
194 
195     /++
196     Converts an image to a texture (i.e. for rendering an image with hardware
197     acceleration). At the same time, now, when the image is called, the texture
198     itself will be drawn. You can get such a texture and change its parameters
199     from the $(LREF "texture") property.
200     +/
201     void toTexture() @trusted
202     {
203         import tida.shape;
204         import tida.vertgen;
205 
206         _texture = new Texture();
207 
208         Shape!float shape = Shape!float.Rectangle(vecf(0, 0), vecf(width, height));
209 
210         TextureInfo info;
211         info.width = _width;
212         info.height = _height;
213         info.params = DefaultParams;
214         info.data = bytes!(PixelFormat.RGBA)();
215 
216         _texture.initializeFromData!(PixelFormat.RGBA)(info);
217         
218         auto vertexInfo = new VertexInfo!float();
219         auto buffer = new BufferInfo!float();
220         auto elements = new ElementInfo!uint();
221 
222         vertexInfo.buffer = buffer;
223         vertexInfo.elements = elements;
224 
225         buffer.append (vecZero!float, 0.0f, 0.0f);
226         buffer.append (vec!float (width, 0), 1.0f, 0.0f);
227         buffer.append (vec!float (width, height), 1.0f, 1.0f);
228         buffer.append (vec!float (0, height), 0.0f, 1.0f);
229 
230         elements.data = [0, 1, 3, 1, 3, 2];
231 
232         buffer.bind();
233         vertexInfo.bind();
234         elements.bind();
235 
236         elements.attach();
237         buffer.attach();
238 
239         vertexInfo.vertexAttribPointer(
240             0,
241             2,
242             4,
243             0
244         );
245 
246         vertexInfo.vertexAttribPointer(
247             1,
248             2,
249             4,
250             2
251         );
252 
253         buffer.unbind();
254         elements.unbind();
255         vertexInfo.unbind();
256 
257         _texture.vertexInfo = vertexInfo;
258         _texture.drawType = ShapeType.rectangle;
259     }
260 
261     /++
262     Converts an image to a texture (i.e. for rendering an image with hardware
263     acceleration). At the same time, now, when the image is called, the texture
264     itself will be drawn. You can get such a texture and change its parameters
265     from the $(LREF "texture") property.
266     +/
267     void toTextureWithoutShape() @trusted
268     {
269         import tida.shape;
270         import tida.vertgen;
271 
272         _texture = new Texture();
273 
274         Shape!float shape = Shape!float.Rectangle(vecf(0, 0), vecf(width, height));
275 
276         TextureInfo info;
277         info.width = _width;
278         info.height = _height;
279         info.params = DefaultParams;
280         info.data = bytes!(PixelFormat.RGBA)();
281 
282         _texture.initializeFromData!(PixelFormat.RGBA)(info);
283     }
284 
285     /++
286     The texture that was generated by the $(LREF "toTexture") function.
287     Using the field, you can set the shape of the texture and its other parameters.
288     +/
289     @property Texture texture() nothrow pure @safe
290     {
291         return _texture;
292     }
293 
294     override void bind(IRenderer render) @safe
295     {
296         if (texture is null)
297             throw new Exception("The texture was not created to bind to the render!");
298         	
299         texture.bind(render);
300     }
301 	
302     override void unbind(IRenderer render) @safe
303     {
304         if (texture is null)
305             throw new Exception("The texture was not created to bind to the render!");
306         	
307         texture.unbind(render);
308     }
309 	
310     override void drawning(IRenderer render) @trusted
311     {
312         import tida.gl;
313     	
314         if (texture is null)
315             throw new Exception("The texture was not created to bind to the render!");
316         	
317         glReadPixels(   0, 0,
318                         width, height, GL_RGBA, GL_UNSIGNED_BYTE, cast(void*) pixels);
319     }
320 
321     override void draw(IRenderer renderer, Vecf position) @trusted
322     {
323         import std.conv : to;
324 
325         if (_texture !is null && renderer.type == RenderType.opengl)
326         {
327             _texture.draw(renderer, position);
328             return;
329         }
330 
331         foreach (x; position.x .. position.x + _width)
332         {
333             foreach (y; position.y .. position.y + _height)
334             {
335                 renderer.point(vecf(x, y),
336                                 getPixel(x.to!int - position.x.to!int,
337                                         y.to!int - position.y.to!int));
338             }
339         }
340     }
341 
342     override void drawEx(   IRenderer renderer,
343                             Vecf position,
344                             float angle,
345                             Vecf center,
346                             Vecf size,
347                             ubyte alpha,
348                             Color!ubyte color) @trusted
349     {
350         import std.conv : to;
351         import tida.angle : rotate;
352 
353         if (texture !is null && renderer.type == RenderType.opengl)
354         {
355             texture.drawEx(renderer, position, angle, center, size, alpha, color);
356             return;
357         }
358 
359         if (!size.isVecfNaN)
360         {
361             Image scaled = this.dup.resize(size.x.to!int, size.y.to!int);
362             scaled.drawEx(renderer, position, angle, center, vecfNaN, alpha, color);
363             return;
364         }
365 
366         if (center.isVecfNaN)
367             center = vecf(width, height) / 2;
368 
369         center += position;
370 
371         foreach (x; position.x .. position.x + _width)
372         {
373             foreach (y; position.y .. position.y + _height)
374             {
375                 Vecf pos = vecf(x,y);
376                 pos = rotate(pos, angle, center);
377 
378                 renderer.point(pos,
379                                 getPixel(x.to!int - position.x.to!int,
380                                          y.to!int - position.y.to!int));
381             }
382         }
383     }
384 
385 @safe nothrow pure:
386     /// Empty constructor image.
387     this() @safe
388     {
389         pixeldata = null;
390     }
391 
392     /++
393     Allocates space for image data.
394 
395     Params:
396         w = Image width.
397         h = Image heght.
398     +/
399     this(uint w, uint h)
400     {
401         this.allocatePlace(w, h);
402     }
403 
404     ref Color!ubyte opIndex(size_t x, size_t y)
405     {
406         return pixeldata[(width * y) + x];
407     }
408 
409     /// Image width.
410     @property uint width()
411     {
412         return _width;
413     }
414 
415     /// Image height.
416     @property uint height()
417     {
418         return _height;
419     }
420 
421     /++
422     Allocates space for image data.
423 
424     Params:
425         w = Image width.
426         h = Image heght.
427     +/
428     void allocatePlace(uint w, uint h)
429     {
430         pixeldata = new Color!ubyte[](w * h);
431 
432         _width = w;
433         _height = h;
434     }
435 
436     /// Image dataset.
437     @property Color!ubyte[] pixels()
438     {
439         return pixeldata;
440     }
441 
442     /// ditto
443     @property Color!ubyte[] pixels(Color!ubyte[] data)
444     in(pixeldata.length == (_width * _height))
445     do
446     {
447         return pixeldata = data;
448     }
449 
450     Color!ubyte[] scanline(uint y)
451     in(y >= 0 && y < height)
452     {
453         return pixeldata[(width * y) .. (width * y) + width];
454     }
455 
456     Color!ubyte[][] scanlines()
457     {
458         return iota(0, height)
459             .map!(e => scanline(e))
460             .array;
461     }
462 
463     /++
464     Gives away bytes as data for an image.
465     +/
466     ubyte[] bytes(int format = PixelFormat.RGBA)()
467     {
468         static assert(isValidFormat!format, CannotDetectAuto);
469 
470         ubyte[] data;
471 
472         foreach (e; pixels)
473             data ~= e.toBytes!format;
474 
475         return data;
476     }
477 
478     /++
479     Accepts bytes as data for an image.
480 
481     Params:
482         data = Image data.
483     +/
484     void bytes(int format = PixelFormat.RGBA)(ubyte[] data)
485     in(validateBytes!format(data), 
486     "The number of bytes is not a multiple of the sample of bytes in a pixel.")
487     in(validateImageData!format(data, _width, _height),
488     "The number of bytes is not a multiple of the image size.")
489     do
490     {
491         pixels = data.fromColors!(format);
492     }
493 
494     /++
495     Whether the data has the current image.
496     +/
497     bool empty()
498     {
499         return pixeldata.length == 0;
500     }
501 
502     /++
503     Fills the data with color.
504 
505     Params:
506         color = The color that will fill the plane of the picture.
507     +/
508     void fill(Color!ubyte color)
509     {
510         pixeldata.fill(color);
511     }
512 
513     /++
514     Sets the pixel to the specified position.
515 
516     Params:
517         x = Pixel x-axis position.
518         y = Pixel y-axis position.
519         color = Pixel color.
520     +/
521     void setPixel(int x, int y, Color!ubyte color)
522     {
523         if (x >= width || y >= height || x < 0 || y < 0)
524             return;
525 
526         pixeldata   [
527                         (width * y) + x
528                     ] = color;  
529     }
530 
531     /++
532     Returns the pixel at the specified location.
533 
534     Params:
535         x = The x-axis position pixel.
536         y = The y-axis position pixel.
537     +/
538     Color!ubyte getPixel(int x,int y)
539     {
540         if (x >= width || y >= height || x < 0 || y < 0) 
541             return Color!ubyte(0, 0, 0, 0);
542 
543         return pixeldata[(width * y) + x];
544     }
545 
546     /++
547     Resizes the image.
548 
549     Params:
550         newWidth = Image new width.
551         newHeight = Image new height.
552     +/
553     Image resize(uint newWidth,uint newHeight)
554     {
555         uint oldWidth = _width;
556         uint oldHeight = _height;
557 
558         _width = newWidth;
559         _height = newHeight;
560 
561         double scaleWidth = cast(double) newWidth / cast(double) oldWidth;
562         double scaleHeight = cast(double) newHeight / cast(double) oldHeight;
563 
564         Color!ubyte[] npixels = new Color!ubyte[](newWidth * newHeight);
565 
566         foreach (cy; 0 .. newHeight)
567         {
568             foreach (cx; 0 .. newWidth)
569             {
570                 uint pixel = (cy * (newWidth)) + (cx);
571                 uint nearestMatch = (cast(uint) (cy / scaleHeight)) * (oldWidth) + (cast(uint) ((cx / scaleWidth)));
572 
573                 npixels[pixel] = pixeldata[nearestMatch];
574             }
575         }
576 
577         pixeldata = npixels;
578 
579         return this;
580     }
581 
582     /++
583     Enlarges the image by a factor.
584 
585     Params:
586         k = factor.
587     +/
588     Image scale(float k)
589     {
590         uint newWidth = cast(uint) ((cast(float) _width) * k);
591         uint newHeight = cast(uint) ((cast(float) _height) * k);
592 
593         return resize(newWidth,newHeight);
594     }
595 
596     /// Dynamic copy of the image.
597     @property Image dup()
598     {
599         return imageFrom(this.pixels.dup, _width, _height);
600     }
601     
602     /// Free image data.
603     void freeData() @trusted
604     {
605         import core.memory;
606 
607         destroy(pixeldata);
608         pixeldata = null;
609         _width = 0;
610         _height = 0;
611     }
612 
613     ~this()
614     {
615         this.freeData();
616     }
617 }
618 
619 unittest
620 {
621     Image image = new Image(32, 32);
622 
623     assert(image.bytes!(PixelFormat.RGBA)
624         .validateImageData!(PixelFormat.RGBA)(image.width, image.height));
625 }
626 
627 alias invert = (x, y, ref e) => e = e.inverted;
628 alias grayscaled = (x, y, ref e) => e = e.toGrayscale;
629 alias changeLightness(float factor) = (x, y, ref e) 
630 { 
631     immutable alpha = e.alpha;
632     e = (e * factor);
633     e.a = alpha;
634 };
635 
636 template process(alias fun)
637 {
638     void process(Image image) @safe nothrow pure
639     {
640         foreach (size_t index, ref Color!ubyte pixel; image.pixels)
641         {
642             immutable y = index / image.width;
643             immutable x = index - (y * image.width);
644 
645             fun(x, y, pixel);
646         }
647     }
648 }
649 
650 /++
651 Rotate the picture by the specified angle from the specified center.
652 
653 Params:
654     image = The picture to be rotated.
655     angle = Angle of rotation.
656     center =    Center of rotation.
657                 (If the vector is empty (non-zero `vecfNan`), then the
658                 center is determined automatically).
659 +/
660 Image rotateImage(int Type = WithoutParallel)(Image image, float angle, Vecf center = vecfNaN) @safe
661 {
662     import tida.angle;
663     import std.conv : to;
664 
665     Image rotated = new Image(image.width, image.height);
666     rotated.fill(rgba(0, 0, 0, 0));
667 
668     if (center.isVecfNaN)
669         center = Vecf(image.width / 2, image.height / 2);
670 
671     foreach (x, y; Coord!Type(image.width, image.height))
672     {
673         auto pos = Vecf(x,y)
674             .rotate(angle, center);
675 
676         auto colorpos = image.getPixel(x,y);
677 
678         rotated.setPixel(pos.x.to!int, pos.y.to!int, colorpos);
679     }
680 
681     return rotated;
682 }
683 
684 enum XAxis = 0; /// X-Axis operation.
685 enum YAxis = 1; /// Y-Axis operation.
686 
687 /++
688 Checks the axis for correctness.
689 +/
690 template isValidAxis(int axistype)
691 {
692     enum isValidAxis = axistype == XAxis || axistype == YAxis;
693 }
694 
695 /++
696 Reverses the picture along the specified axis.
697 
698 Example:
699 ---
700 image
701     .flip!XAxis
702     .flip!YAxis;
703 ---
704 +/
705 template flipImpl(int axis)
706 {
707     static if (axis == XAxis)
708     {
709         Image flipImpl(Image image) @trusted nothrow pure
710         {
711             import std.algorithm : joiner, map;
712 
713             return image.scanlines
714                 .map!(e => e.reversed)
715                 .joiner
716                 .imageFrom(image.width, image.height);
717 
718         }
719     } else
720     static if (axis == YAxis)
721     {
722         Image flipImpl(Image image) @trusted nothrow pure
723         {
724             import std.algorithm : reverse, joiner;
725 
726             return image
727                 .scanlines
728                 .reversed
729                 .joiner
730                 .imageFrom(image.width, image.height);
731         }   
732     }
733 }
734 
735 alias flip = flipImpl;
736 
737 alias flipX = flipImpl!XAxis;
738 alias flipY = flipImpl!YAxis;
739 
740 /++
741 Save image in file.
742 
743 Params:
744     image = Image.
745     path = Path to the file.
746 +/
747 void saveImageInFile(Image image, string path) @trusted
748 in(!image.empty, "The image can not be empty!")
749 do
750 {
751     import imagefmt;
752     import tida.color;
753 
754     write_image(path, image.width, image.height, image.bytes!(PixelFormat.RGBA), 4);
755 }
756 
757 /++
758 Divides the picture into frames.
759 
760 Params:
761     image = Atlas.
762     x = Begin x-axis position divide.
763     y = Begin y-axis position divide.
764     w = Frame width.
765     h = Frame height.
766 +/
767 Image[] strip(Image image, int x, int y, int w, int h) @safe
768 {
769     import std.algorithm : map;
770     import std.range : iota, array;
771 
772     return iota(0, image.width / w)
773         .map!(e => image.copy(e * w, y, w, h))
774         .array;
775 }
776 
777 /++
778 Combining two paintings into one using color mixing.
779 
780 Params:
781     a = First image.
782     b = Second image.
783     posA = First image position.
784     posB = Second image position.
785 +/
786 Image unite(int Type = WithoutParallel)(Image a,
787                                         Image b,
788                                         Vecf posA = vecf(0, 0),
789                                         Vecf posB = vecf(0, 0)) @trusted
790 {
791     import std.conv : to;
792     import tida.color;
793 
794     Image result = new Image();
795 
796     int width = int.init,
797         height = int.init;
798 
799     width = (posA.x + a.width > posB.x + b.width) ? posA.x.to!int + a.width : posB.x.to!int + b.width;
800     height = (posA.y + a.height > posB.y + b.height) ? posA.y.to!int + a.height : posB.x.to!int + b.height;
801 
802     result.allocatePlace(width, height);
803     result.fill(rgba(0, 0, 0, 0));
804 
805     foreach (x, y; Coord!Type(  posA.x.to!int + a.width, posA.y.to!int + a.height,
806                                 posA.x.to!int, posA.y.to!int))
807     {
808         Color!ubyte color = a.getPixel(x - posA.x.to!int, y - posA.y.to!int);
809         result.setPixel(x,y,color);
810     }
811 
812     foreach (x, y; Coord!Type(  posB.x.to!int + b.width, posB.y.to!int + b.height,
813                                 posB.x.to!int, posB.y.to!int))
814     {
815         Color!ubyte color = b.getPixel(x - posB.x.to!int, y - posB.y.to!int);
816         Color!ubyte backColor = result.getPixel(x, y);
817         color = color.BlendAlpha!ubyte(backColor);
818         result.setPixel(x, y, color);
819     }
820 
821     return result;
822 }
823 
824 /++
825 Generates a gauss matrix.
826 
827 Params:
828     width = Matrix width.
829     height = Matrix height.
830     sigma = Radious gaus.
831 
832 Return: Matrix
833 +/
834 float[][] gausKernel(int width, int height, float sigma) @safe nothrow pure
835 {
836     import std.math : exp, PI;
837 
838     float[][] result = new float[][](width, height);
839 
840     float sum = 0f;
841 
842     foreach (i; 0 .. height)
843     {
844         foreach (j; 0 .. width)
845         {
846             result[j][i] = exp(-(i * i + j * j) / (2 * sigma * sigma) / (2 * PI * sigma * sigma));
847             sum += result[j][i];
848         }
849     }
850 
851     foreach (i; 0 .. height)
852     {
853         foreach (j; 0 .. width)
854         {
855             result[j][i] /= sum;
856         }
857     }
858 
859     return result;
860 }
861 
862 /++
863 Generates a gauss matrix.
864 
865 Params:
866     r = Radiuos gaus.
867 +/
868 float[][] gausKernel(float r) @safe nothrow pure
869 {
870     return gausKernel(cast(int) (r * 2), cast(int) (r * 2), r);
871 }
872 
873 /++
874 Applies blur.
875 
876 Params:
877     Type = Operation type.
878     image = Image.
879     r = radius gaus kernel.
880 +/
881 Image blur(int Type = WithoutParallel)(Image image, float r) @safe
882 {
883     return blur!Type(image, gausKernel(r));
884 }
885 
886 /++
887 Apolies blur.
888 
889 Params:
890     Type = Operation type.
891     image = Image.
892     width = gaus kernel width.
893     height = gaus kernel height.
894     r = radius gaus kernel.
895 +/
896 Image blur(int Type = WithoutParallel)(Image image, int width, int height, float r) @safe
897 {
898     return blur!Type(image, gausKernel(width, height, r));
899 }
900 
901 /++
902 Applies blur.
903 
904 Params:
905     Type = Operation type.
906     image = Image.
907     otherKernel = Filter kernel.
908 +/
909 Image blur(int Type = WithoutParallel)(Image image, float[][] otherKernel) @trusted
910 {
911     import tida.color;
912 
913     auto kernel = otherKernel; 
914     
915     int width = image.width;
916     int height = image.height;
917 
918     int kernelWidth = cast(int) kernel.length;
919     int kernelHeight = cast(int) kernel[0].length;
920 
921     Image result = new Image(width,height);
922     result.fill(rgba(0,0,0,0));
923 
924     foreach (x, y; Coord!Type(width, height))
925     {
926         Color!ubyte color = rgb(0,0,0);
927 
928         foreach (ix,iy; Coord(kernelWidth, kernelHeight))
929         {
930             color = color + (image.getPixel(x - kernelWidth / 2 + ix,y - kernelHeight / 2 + iy) * (kernel[ix][iy]));
931         }
932 
933         color.a = image.getPixel(x,y).a;
934         result.setPixel(x,y,color);
935     }
936 
937     return result;
938 }