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 }