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 }