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 }