1 /+ 2 == pixmappaint == 3 Copyright Elias Batek (0xEAB) 2024. 4 Distributed under the Boost Software License, Version 1.0. 5 +/ 6 /++ 7 Pixmap image manipulation 8 9 $(WARNING 10 $(B Early Technology Preview.) 11 ) 12 13 $(PITFALL 14 This module is $(B work in progress). 15 API is subject to changes until further notice. 16 ) 17 18 Pixmap refers to raster graphics, a subset of “bitmap” graphics. 19 A pixmap is an array of pixels and the corresponding meta data to describe 20 how an image if formed from those pixels. 21 In the case of this library, a “width” field is used to map a specified 22 number of pixels to a row of an image. 23 24 ``` 25 pixels := [ 0, 1, 2, 3 ] 26 width := 2 27 28 pixmap(pixels, width) 29 => [ 30 [ 0, 1 ] 31 [ 2, 3 ] 32 ] 33 ``` 34 35 ``` 36 pixels := [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] 37 width := 3 38 39 pixmap(pixels, width) 40 => [ 41 [ 0, 1, 2 ] 42 [ 3, 4, 5 ] 43 [ 6, 7, 8 ] 44 [ 9, 10, 11 ] 45 ] 46 ``` 47 48 ``` 49 pixels := [ 0, 1, 2, 3, 4, 5, 6, 7 ] 50 width := 4 51 52 pixmap(pixels, width) 53 => [ 54 [ 0, 1, 2, 3 ] 55 [ 4, 5, 6, 7 ] 56 ] 57 ``` 58 59 60 61 62 ### Colors 63 64 Colors are stored in an RGBA format with 8 bit per channel. 65 See [arsd.color.Color|Pixel] for details. 66 67 68 ### The coordinate system 69 70 The top left corner of a pixmap is its $(B origin) `(0,0)`. 71 72 The $(horizontal axis) is called `x`. 73 Its corresponding length/dimension is known as `width`. 74 75 The letter `y` is used to describe the $(B vertical axis). 76 Its corresponding length/dimension is known as `height`. 77 78 ``` 79 0 → x 80 ↓ 81 y 82 ``` 83 84 Furthermore, $(B length) refers to the areal size of a pixmap. 85 It represents the total number of pixels in a pixmap. 86 It follows from the foregoing that the term $(I long) usually refers to 87 the length (not the width). 88 89 90 91 92 ### Pixmaps 93 94 A [Pixmap] consist of two fields: 95 $(LIST 96 * a slice (of an array of [Pixel|Pixels]) 97 * a width 98 ) 99 100 This design comes with many advantages. 101 First and foremost it brings simplicity. 102 103 Pixel data buffers can be reused across pixmaps, 104 even when those have different sizes. 105 Simply slice the buffer to fit just enough pixels for the new pixmap. 106 107 Memory management can also happen outside of the pixmap. 108 It is possible to use a buffer allocated elsewhere. (Such a one shouldn’t 109 be mixed with the built-in memory management facilities of the pixmap type. 110 Otherwise one will end up with GC-allocated copies.) 111 112 The most important downside is that it makes pixmaps basically a partial 113 reference type. 114 115 Copying a pixmap creates a shallow copy still poiting to the same pixel 116 data that is also used by the source pixmap. 117 This implies that manipulating the source pixels also manipulates the 118 pixels of the copy – and vice versa. 119 120 The issues implied by this become an apparent when one of the references 121 modifies the pixel data in a way that also affects the dimensions of the 122 image; such as cropping. 123 124 Pixmaps describe how pixel data stored in a 1-dimensional memory space is 125 meant to be interpreted as a 2-dimensional image. 126 127 A notable implication of this 1D ↔ 2D mapping is, that slicing the 1D data 128 leads to non-sensical results in the 2D space when the 1D-slice is 129 reinterpreted as 2D-image. 130 131 Especially slicing across scanlines (→ horizontal rows of an image) is 132 prone to such errors. 133 134 (Slicing of the 1D array data can actually be utilized to cut off the 135 bottom part of an image. Any other naiv cropping operations will run into 136 the aforementioned issues.) 137 138 139 140 141 ### Image manipulation 142 143 The term “image manipulation function” here refers to functions that 144 manipulate (e.g. transform) an image as a whole. 145 146 Image manipulation functions in this library are provided in up to three 147 flavors: 148 149 $(LIST 150 * a “source to target” function 151 * a “source to newly allocated target” wrapper 152 * $(I optionally) an “in-place” adaption 153 ) 154 155 Additionally, a “compute dimensions of target” function is provided. 156 157 158 #### Source to Target 159 160 The regular “source to target” function takes (at least) two parameters: 161 A source [Pixmap] and a target [Pixmap]. 162 163 (Additional operation-specific arguments may be required as well.) 164 165 The target pixmap usually needs to be able to fit at least the same number 166 of pixels as the source holds. 167 Use the corresponding “compute size of target function” to calculate the 168 required size when needed. 169 (A notable exception would be cropping, where to target pixmap must be only 170 at least long enough to hold the area of the size to crop to.) 171 172 The data stored in the buffer of the target pixmap is overwritten by the 173 operation. 174 175 A modified Pixmap structure with adjusted dimensions is returned. 176 177 These functions are named plain and simple after the respective operation 178 they perform; e.g. [flipHorizontally] or [crop]. 179 180 --- 181 // Allocate a new target Pixmap. 182 Pixmap target = Pixmap.makeNew( 183 flipHorizontallyCalcDims(sourceImage) 184 ); 185 186 // Flip the image horizontally and store the updated structure. 187 // (Note: As a horizontal flip does not affect the dimensions of a Pixmap, 188 // storing the updated structure would not be necessary 189 // in this specific scenario.) 190 target = sourceImage.flipHorizontally(target); 191 --- 192 193 --- 194 const cropOffset = Point(0, 0); 195 const cropSize = Size(100, 100); 196 197 // Allocate a new target Pixmap. 198 Pixmap target = Pixmap.makeNew( 199 cropCalcDims(sourceImage, cropSize, cropOffset) 200 ); 201 202 // Crop the Pixmap. 203 target = sourceImage.crop(target, cropSize, cropOffset); 204 --- 205 206 $(PITFALL 207 “Source to target” functions do not work in place. 208 Do not attempt to pass Pixmaps sharing the same buffer for both source 209 and target. Such would lead to bad results with heavy artifacts. 210 211 Use the “in-place” variant of the operation instead. 212 213 Moreover: 214 Do not use the artifacts produced by this as a creative effect. 215 Those are an implementation detail (and may change at any point). 216 ) 217 218 219 #### Source to New Target 220 221 The “source to newly allocated target” wrapper allocates a new buffer to 222 hold the manipulated target. 223 224 These wrappers are provided for user convenience. 225 226 They are identified by the suffix `-New` that is appended to the name of 227 the corresponding “source to target” function; 228 e.g. [flipHorizontallyNew] or [cropNew]. 229 230 --- 231 // Create a new flipped Pixmap. 232 Pixmap target = sourceImage.flipHorizontallyNew(); 233 --- 234 235 --- 236 const cropOffset = Point(0, 0); 237 const cropSize = Size(100, 100); 238 239 // Create a new cropped Pixmap. 240 Pixmap target = sourceImage.cropNew(cropSize, cropOffset); 241 --- 242 243 244 #### In-Place 245 246 For selected image manipulation functions a special adaption is provided 247 that stores the result in the source pixel data buffer. 248 249 Depending on the operation, implementing in-place transformations can be 250 either straightforward or a major undertaking (and topic of research). 251 This library focuses and the former and leaves out cases where the latter 252 applies. 253 In particular, algorithms that require allocating further buffers to store 254 temporary results or auxiliary data will probably not get implemented. 255 256 Furthermore, operations where to result is longer than the source cannot 257 be performed in-place. 258 259 Certain in-place manipulation functions return a shallow-copy of the 260 source structure with dimensions adjusted accordingly. 261 This is behavior is not streamlined consistently as the lack of an 262 in-place option for certain operations makes them a special case anyway. 263 264 These function are suffixed with `-InPlace`; 265 e.g. [flipHorizontallyInPlace] or [cropInPlace]. 266 267 $(TIP 268 Manipulating the source image directly can lead to unexpected results 269 when the source image is used in multiple places. 270 ) 271 272 $(NOTE 273 Users are usually better off to utilize the regular “source to target” 274 functions with a reused pixel data buffer. 275 276 These functions do not serve as a performance optimization. 277 Some of them might perform significantly worse than their regular 278 variant. Always benchmark and profile. 279 ) 280 281 --- 282 image.flipHorizontallyInPlace(); 283 --- 284 285 --- 286 const cropOffset = Point(0, 0); 287 const cropSize = Size(100, 100); 288 289 image = image.cropInPlace(cropSize, cropOffset); 290 --- 291 292 293 #### Compute size of target 294 295 Functions to “compute (the) dimensions of (a) target” are primarily meant 296 to be utilized to calculate the size for allocating new pixmaps to be used 297 as a target for manipulation functions. 298 299 They are provided for all manipulation functions even in cases where they 300 are provide little to no benefit. This is for consistency and to ease 301 development. 302 303 Such functions are identified by a `-CalcDims` suffix; 304 e.g. [flipHorizontallyCalcDims] or [cropCalcDims]. 305 306 They usually take the same parameters as their corresponding 307 “source to new target” function. This does not apply in cases where 308 certain parameters are irrelevant for the computation of the target size. 309 +/ 310 module arsd.pixmappaint; 311 312 import arsd.color; 313 import arsd.core; 314 315 private float roundImpl(float f) { 316 import std.math : round; 317 318 return round(f); 319 } 320 321 // `pure` rounding function. 322 // std.math.round() isn’t pure on all targets. 323 // → <https://issues.dlang.org/show_bug.cgi?id=11320> 324 private float round(float f) pure @nogc nothrow @trusted { 325 return (castTo!(float function(float) pure @nogc nothrow)(&roundImpl))(f); 326 } 327 328 /* 329 ## TODO: 330 331 - Refactoring the template-mess of blendPixel() & co. 332 - Scaling 333 - Rotating 334 - Skewing 335 - HSL 336 - Advanced blend modes (maybe) 337 */ 338 339 /// 340 alias Color = arsd.color.Color; 341 342 /// 343 alias ColorF = arsd.color.ColorF; 344 345 /// 346 alias Pixel = Color; 347 348 /// 349 alias Point = arsd.color.Point; 350 351 /// 352 alias Rectangle = arsd.color.Rectangle; 353 354 /// 355 alias Size = arsd.color.Size; 356 357 // verify assumption(s) 358 static assert(Pixel.sizeof == uint.sizeof); 359 360 @safe pure nothrow @nogc { 361 /// 362 Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) { 363 return Pixel(r, g, b, a); 364 } 365 366 /// 367 Pixel rgba(ubyte r, ubyte g, ubyte b, float aPct) { 368 return Pixel(r, g, b, percentageDecimalToUInt8(aPct)); 369 } 370 371 /// 372 Pixel rgb(ubyte r, ubyte g, ubyte b) { 373 return rgba(r, g, b, 0xFF); 374 } 375 } 376 377 /++ 378 $(I Advanced functionality.) 379 380 Meta data for the construction of a Pixmap. 381 +/ 382 struct PixmapBlueprint { 383 /++ 384 Total number of pixels stored in a Pixmap. 385 +/ 386 size_t length; 387 388 /++ 389 Width of a Pixmap. 390 +/ 391 int width; 392 393 @safe pure nothrow @nogc: 394 395 /// 396 public static PixmapBlueprint fromSize(const Size size) { 397 return PixmapBlueprint( 398 size.area, 399 size.width, 400 ); 401 } 402 403 /// 404 public static PixmapBlueprint fromPixmap(const Pixmap pixmap) { 405 return PixmapBlueprint( 406 pixmap.length, 407 pixmap.width, 408 ); 409 } 410 411 /++ 412 Determines whether the blueprint is plausible. 413 +/ 414 bool isValid() const { 415 return ((length % width) == 0); 416 } 417 418 /++ 419 Height of a Pixmap. 420 421 See_also: 422 This is the counterpart to the dimension known as [width]. 423 +/ 424 int height() const { 425 return castTo!int(length / width); 426 } 427 428 /// 429 Size size() const { 430 return Size(width, height); 431 } 432 } 433 434 /++ 435 Pixel data container 436 +/ 437 struct Pixmap { 438 439 /// Pixel data 440 Pixel[] data; 441 442 /// Pixel per row 443 int width; 444 445 @safe pure nothrow: 446 447 /// 448 deprecated("Use `Pixmap.makeNew(size)` instead.") 449 this(Size size) { 450 this.size = size; 451 } 452 453 /// 454 deprecated("Use `Pixmap.makeNew(Size(width, height))` instead.") 455 this(int width, int height) 456 in (width > 0) 457 in (height > 0) { 458 this(Size(width, height)); 459 } 460 461 /// 462 this(inout(Pixel)[] data, int width) inout @nogc 463 in (data.length % width == 0) { 464 this.data = data; 465 this.width = width; 466 } 467 468 /// 469 static Pixmap makeNew(PixmapBlueprint blueprint) { 470 auto data = new Pixel[](blueprint.length); 471 return Pixmap(data, blueprint.width); 472 } 473 474 /// 475 static Pixmap makeNew(Size size) { 476 return Pixmap.makeNew(PixmapBlueprint.fromSize(size)); 477 } 478 479 /++ 480 Creates a $(I deep copy) of the Pixmap 481 +/ 482 Pixmap clone() const { 483 return Pixmap( 484 this.data.dup, 485 this.width, 486 ); 487 } 488 489 /++ 490 Copies the pixel data to the target Pixmap. 491 492 Returns: 493 A size-adjusted shallow copy of the input Pixmap overwritten 494 with the image data of the SubPixmap. 495 496 $(PITFALL 497 While the returned Pixmap utilizes the buffer provided by the input, 498 the returned Pixmap might not exactly match the input. 499 500 Always use the returned Pixmap structure. 501 502 --- 503 // Same buffer, but new structure: 504 auto pixmap2 = source.copyTo(pixmap); 505 506 // Alternatively, replace the old structure: 507 pixmap = source.copyTo(pixmap); 508 --- 509 ) 510 +/ 511 Pixmap copyTo(Pixmap target) @nogc const { 512 // Length adjustment 513 const l = this.length; 514 if (target.data.length < l) { 515 assert(false, "The target Pixmap is too small."); 516 } else if (target.data.length > l) { 517 target.data = target.data[0 .. l]; 518 } 519 520 copyToImpl(target); 521 522 return target; 523 } 524 525 private void copyToImpl(Pixmap target) @nogc const { 526 target.data[] = this.data[]; 527 } 528 529 // undocumented: really shouldn’t be used. 530 // carries the risks of `length` and `width` getting out of sync accidentally. 531 deprecated("Use `size` instead.") 532 void length(int value) { 533 data.length = value; 534 } 535 536 /++ 537 Changes the size of the buffer 538 539 Reallocates the underlying pixel array. 540 +/ 541 void size(Size value) { 542 data.length = value.area; 543 width = value.width; 544 } 545 546 /// ditto 547 void size(int totalPixels, int width) 548 in (totalPixels % width == 0) { 549 data.length = totalPixels; 550 this.width = width; 551 } 552 553 static { 554 /++ 555 Creates a Pixmap wrapping the pixel data from the provided `TrueColorImage`. 556 557 Interoperability function: `arsd.color` 558 +/ 559 Pixmap fromTrueColorImage(TrueColorImage source) @nogc { 560 return Pixmap(source.imageData.colors, source.width); 561 } 562 563 /++ 564 Creates a Pixmap wrapping the pixel data from the provided `MemoryImage`. 565 566 Interoperability function: `arsd.color` 567 +/ 568 Pixmap fromMemoryImage(MemoryImage source) { 569 return fromTrueColorImage(source.getAsTrueColorImage()); 570 } 571 } 572 573 @safe pure nothrow @nogc: 574 575 /// Height of the buffer, i.e. the number of lines 576 int height() inout { 577 if (width == 0) { 578 return 0; 579 } 580 581 return castTo!int(data.length / width); 582 } 583 584 /// Rectangular size of the buffer 585 Size size() inout { 586 return Size(width, height); 587 } 588 589 /// Length of the buffer, i.e. the number of pixels 590 int length() inout { 591 return castTo!int(data.length); 592 } 593 594 /++ 595 Number of bytes per line 596 597 Returns: 598 width × Pixel.sizeof 599 +/ 600 int pitch() inout { 601 return (width * int(Pixel.sizeof)); 602 } 603 604 /++ 605 Adjusts the Pixmap according to the provided blueprint. 606 607 The blueprint must not be larger than the data buffer of the pixmap. 608 609 This function does not reallocate the pixel data buffer. 610 611 If the blueprint is larger than the data buffer of the pixmap, 612 this will result in a bounds-check error if applicable. 613 +/ 614 void adjustTo(PixmapBlueprint blueprint) { 615 debug assert(this.data.length >= blueprint.length); 616 debug assert(blueprint.isValid); 617 this.data = this.data[0 .. blueprint.length]; 618 this.width = blueprint.width; 619 } 620 621 /++ 622 Calculates the index (linear offset) of the requested position 623 within the pixmap data. 624 +/ 625 int scanTo(Point pos) inout { 626 return linearOffset(width, pos); 627 } 628 629 /++ 630 Accesses the pixel at the requested position within the pixmap data. 631 +/ 632 ref inout(Pixel) scan(Point pos) inout { 633 return data[scanTo(pos)]; 634 } 635 636 /++ 637 Retrieves a linear slice of the pixmap. 638 639 Returns: 640 `n` pixels starting at the top-left position `pos`. 641 +/ 642 inout(Pixel)[] scan(Point pos, int n) inout { 643 immutable size_t offset = linearOffset(width, pos); 644 immutable size_t end = (offset + n); 645 return data[offset .. end]; 646 } 647 648 /// ditto 649 inout(Pixel)[] sliceAt(Point pos, int n) inout { 650 return scan(pos, n); 651 } 652 653 /++ 654 Retrieves a rectangular subimage of the pixmap. 655 +/ 656 inout(SubPixmap) scanArea(Point pos, Size size) inout { 657 return inout(SubPixmap)(this, size, pos); 658 } 659 660 /// TODO: remove 661 deprecated alias scanSubPixmap = scanArea; 662 663 /// TODO: remove 664 deprecated alias scan2D = scanArea; 665 666 /++ 667 Retrieves the first line of the Pixmap. 668 669 See_also: 670 Check out [PixmapScanner] for more useful scanning functionality. 671 +/ 672 inout(Pixel)[] scanLine() inout { 673 return data[0 .. width]; 674 } 675 676 public { 677 /++ 678 Provides access to a single pixel at the requested 2D-position. 679 680 See_also: 681 Accessing pixels through the [data] array will be more useful, 682 usually. 683 +/ 684 ref inout(Pixel) accessPixel(Point pos) inout @system { 685 const idx = linearOffset(pos, this.width); 686 return this.data[idx]; 687 } 688 689 /// ditto 690 Pixel getPixel(Point pos) const { 691 const idx = linearOffset(pos, this.width); 692 return this.data[idx]; 693 } 694 695 /// ditto 696 Pixel getPixel(int x, int y) const { 697 return this.getPixel(Point(x, y)); 698 } 699 700 /// ditto 701 void setPixel(Point pos, Pixel value) { 702 const idx = linearOffset(pos, this.width); 703 this.data[idx] = value; 704 } 705 706 /// ditto 707 void setPixel(int x, int y, Pixel value) { 708 return this.setPixel(Point(x, y), value); 709 } 710 } 711 712 /// Clears the buffer’s contents (by setting each pixel to the same color) 713 void clear(Pixel value) { 714 data[] = value; 715 } 716 } 717 718 /++ 719 A subpixmap represents a subimage of a [Pixmap]. 720 721 This wrapper provides convenient access to a rectangular slice of a Pixmap. 722 723 ``` 724 ╔═════════════╗ 725 ║ Pixmap ║ 726 ║ ║ 727 ║ ┌───┐ ║ 728 ║ │Sub│ ║ 729 ║ └───┘ ║ 730 ╚═════════════╝ 731 ``` 732 +/ 733 struct SubPixmap { 734 735 /++ 736 Source image referenced by the subimage 737 +/ 738 Pixmap source; 739 740 /++ 741 Size of the subimage 742 +/ 743 Size size; 744 745 /++ 746 2D offset of the subimage 747 +/ 748 Point offset; 749 750 public @safe pure nothrow @nogc { 751 /// 752 this(inout Pixmap source, Size size = Size(0, 0), Point offset = Point(0, 0)) inout { 753 this.source = source; 754 this.size = size; 755 this.offset = offset; 756 } 757 758 /// 759 this(inout Pixmap source, Point offset, Size size = Size(0, 0)) inout { 760 this(source, size, offset); 761 } 762 } 763 764 @safe pure nothrow: 765 766 public { 767 /++ 768 Allocates a new Pixmap cropped to the pixel data of the subimage. 769 770 See_also: 771 Use [extractToPixmap] for a non-allocating variant with an . 772 +/ 773 Pixmap extractToNewPixmap() const { 774 auto pm = Pixmap.makeNew(size); 775 this.extractToPixmap(pm); 776 return pm; 777 } 778 779 /++ 780 Copies the pixel data – cropped to the subimage region – 781 into the target Pixmap. 782 783 $(PITFALL 784 Do not attempt to extract a subimage back into the source pixmap. 785 This will fail in cases where source and target regions overlap 786 and potentially crash the program. 787 ) 788 789 Returns: 790 A size-adjusted shallow copy of the input Pixmap overwritten 791 with the image data of the SubPixmap. 792 793 $(PITFALL 794 While the returned Pixmap utilizes the buffer provided by the input, 795 the returned Pixmap might not exactly match the input. 796 The dimensions (width and height) and the length might have changed. 797 798 Always use the returned Pixmap structure. 799 800 --- 801 // Same buffer, but new structure: 802 auto pixmap2 = subPixmap.extractToPixmap(pixmap); 803 804 // Alternatively, replace the old structure: 805 pixmap = subPixmap.extractToPixmap(pixmap); 806 --- 807 ) 808 +/ 809 Pixmap extractToPixmap(Pixmap target) @nogc const { 810 // Length adjustment 811 const l = this.length; 812 if (target.data.length < l) { 813 assert(false, "The target Pixmap is too small."); 814 } else if (target.data.length > l) { 815 target.data = target.data[0 .. l]; 816 } 817 818 target.width = this.width; 819 820 extractToPixmapCopyImpl(target); 821 return target; 822 } 823 824 private void extractToPixmapCopyImpl(Pixmap target) @nogc const { 825 auto src = SubPixmapScanner(this); 826 auto dst = PixmapScannerRW(target); 827 828 foreach (dstLine; dst) { 829 dstLine[] = src.front[]; 830 src.popFront(); 831 } 832 } 833 834 private void extractToPixmapCopyPixelByPixelImpl(Pixmap target) @nogc const { 835 auto src = SubPixmapScanner(this); 836 auto dst = PixmapScannerRW(target); 837 838 foreach (dstLine; dst) { 839 const srcLine = src.front; 840 foreach (idx, ref px; dstLine) { 841 px = srcLine[idx]; 842 } 843 src.popFront(); 844 } 845 } 846 } 847 848 @safe pure nothrow @nogc: 849 850 public { 851 /++ 852 Width of the subimage. 853 +/ 854 int width() const { 855 return size.width; 856 } 857 858 /// ditto 859 void width(int value) { 860 size.width = value; 861 } 862 863 /++ 864 Height of the subimage. 865 +/ 866 int height() const { 867 return size.height; 868 } 869 870 /// ditto 871 void height(int value) { 872 size.height = value; 873 } 874 875 /++ 876 Number of pixels in the subimage. 877 +/ 878 int length() const { 879 return size.area; 880 } 881 } 882 883 public { 884 /++ 885 Linear offset of the subimage within the source image. 886 887 Calculates the index of the “first pixel of the subimage” 888 in the “pixel data of the source image”. 889 +/ 890 int sourceOffsetLinear() const { 891 return linearOffset(offset, source.width); 892 } 893 894 /// ditto 895 void sourceOffsetLinear(int value) { 896 this.offset = Point.fromLinearOffset(value, source.width); 897 } 898 899 /++ 900 $(I Advanced functionality.) 901 902 Offset of the pixel following the bottom right corner of the subimage. 903 904 (`Point(O, 0)` is the top left corner of the source image.) 905 +/ 906 Point sourceOffsetEnd() const { 907 auto vec = Point(size.width, (size.height - 1)); 908 return (offset + vec); 909 } 910 911 /++ 912 Linear offset of the subimage within the source image. 913 914 Calculates the index of the “first pixel of the subimage” 915 in the “pixel data of the source image”. 916 +/ 917 int sourceOffsetLinearEnd() const { 918 return linearOffset(sourceOffsetEnd, source.width); 919 } 920 } 921 922 /++ 923 Determines whether the area of the subimage 924 lies within the source image 925 and does not overflow its lines. 926 927 $(TIP 928 If the offset and/or size of a subimage are off, two issues can occur: 929 930 $(LIST 931 * The resulting subimage will look displaced. 932 (As if the lines were shifted.) 933 This indicates that one scanline of the subimage spans over 934 two ore more lines of the source image. 935 (Happens when `(subimage.offset.x + subimage.size.width) > source.size.width`.) 936 * When accessing the pixel data, bounds checks will fail. 937 This suggests that the area of the subimage extends beyond 938 the bottom end (and optionally also beyond the right end) of 939 the source. 940 ) 941 942 Both defects could indicate an invalid subimage. 943 Use this function to verify the SubPixmap. 944 ) 945 946 $(WARNING 947 Do not use invalid SubPixmaps. 948 The library assumes that the SubPixmaps it receives are always valid. 949 950 Non-valid SubPixmaps are not meant to be used for creative effects 951 or similar either. Such uses might lead to unexpected quirks or 952 crashes eventually. 953 ) 954 +/ 955 bool isValid() const { 956 return ( 957 (sourceMarginLeft >= 0) 958 && (sourceMarginTop >= 0) 959 && (sourceMarginBottom >= 0) 960 && (sourceMarginRight >= 0) 961 ); 962 } 963 964 public inout { 965 /++ 966 Retrieves the pixel at the requested position of the subimage. 967 +/ 968 ref inout(Pixel) scan(Point pos) { 969 return source.scan(offset + pos); 970 } 971 972 /++ 973 Retrieves the first line of the subimage. 974 +/ 975 inout(Pixel)[] scanLine() { 976 const lo = linearOffset(offset, size.width); 977 return source.data[lo .. size.width]; 978 } 979 } 980 981 /++ 982 Copies the pixels of this subimage to a target image. 983 984 The target MUST have the same size. 985 986 See_also: 987 Usually you’ll want to use [extractToPixmap] or [drawPixmap] instead. 988 +/ 989 public void xferTo(SubPixmap target) const { 990 debug assert(target.size == this.size); 991 992 auto src = SubPixmapScanner(this); 993 auto dst = SubPixmapScannerRW(target); 994 995 foreach (dstLine; dst) { 996 dstLine[] = src.front[]; 997 src.popFront(); 998 } 999 } 1000 1001 /++ 1002 Blends the pixels of this subimage into a target image. 1003 1004 The target MUST have the same size. 1005 1006 See_also: 1007 Usually you’ll want to use [extractToPixmap] or [drawPixmap] instead. 1008 +/ 1009 public void xferTo(SubPixmap target, Blend blend) const { 1010 debug assert(target.size == this.size); 1011 1012 auto src = SubPixmapScanner(this); 1013 auto dst = SubPixmapScannerRW(target); 1014 1015 foreach (dstLine; dst) { 1016 blendPixels(dstLine, src.front, blend); 1017 src.popFront(); 1018 } 1019 } 1020 1021 // opposite offset 1022 public const { 1023 /++ 1024 $(I Advanced functionality.) 1025 1026 Offset of the bottom right corner of the source image 1027 to the bottom right corner of the subimage. 1028 1029 ``` 1030 ╔═══════════╗ 1031 ║ ║ 1032 ║ ┌───┐ ║ 1033 ║ │ │ ║ 1034 ║ └───┘ ║ 1035 ║ ↘ ║ 1036 ╚═══════════╝ 1037 ``` 1038 +/ 1039 Point oppositeOffset() { 1040 return Point(oppositeOffsetX, oppositeOffsetY); 1041 } 1042 1043 /++ 1044 $(I Advanced functionality.) 1045 1046 Offset of the right edge of the source image 1047 to the right edge of the subimage. 1048 1049 ``` 1050 ╔═══════════╗ 1051 ║ ║ 1052 ║ ┌───┐ ║ 1053 ║ │ S │ → ║ 1054 ║ └───┘ ║ 1055 ║ ║ 1056 ╚═══════════╝ 1057 ``` 1058 +/ 1059 int oppositeOffsetX() { 1060 return (offset.x + size.width); 1061 } 1062 1063 /++ 1064 $(I Advanced functionality.) 1065 1066 Offset of the bottom edge of the source image 1067 to the bottom edge of the subimage. 1068 1069 ``` 1070 ╔═══════════╗ 1071 ║ ║ 1072 ║ ┌───┐ ║ 1073 ║ │ S │ ║ 1074 ║ └───┘ ║ 1075 ║ ↓ ║ 1076 ╚═══════════╝ 1077 ``` 1078 +/ 1079 int oppositeOffsetY() { 1080 return (offset.y + size.height); 1081 } 1082 1083 } 1084 1085 // source-image margins 1086 public const { 1087 /++ 1088 $(I Advanced functionality.) 1089 1090 X-axis margin (left + right) of the subimage within the source image. 1091 1092 ``` 1093 ╔═══════════╗ 1094 ║ ║ 1095 ║ ┌───┐ ║ 1096 ║ ↔ │ S │ ↔ ║ 1097 ║ └───┘ ║ 1098 ║ ║ 1099 ╚═══════════╝ 1100 ``` 1101 +/ 1102 int sourceMarginX() { 1103 return (source.width - size.width); 1104 } 1105 1106 /++ 1107 $(I Advanced functionality.) 1108 1109 Y-axis margin (top + bottom) of the subimage within the source image. 1110 1111 ``` 1112 ╔═══════════╗ 1113 ║ ↕ ║ 1114 ║ ┌───┐ ║ 1115 ║ │ S │ ║ 1116 ║ └───┘ ║ 1117 ║ ↕ ║ 1118 ╚═══════════╝ 1119 ``` 1120 +/ 1121 int sourceMarginY() { 1122 return (source.height - size.height); 1123 } 1124 1125 /++ 1126 $(I Advanced functionality.) 1127 1128 Top margin of the subimage within the source image. 1129 1130 ``` 1131 ╔═══════════╗ 1132 ║ ↕ ║ 1133 ║ ┌───┐ ║ 1134 ║ │ S │ ║ 1135 ║ └───┘ ║ 1136 ║ ║ 1137 ╚═══════════╝ 1138 ``` 1139 +/ 1140 int sourceMarginTop() { 1141 return offset.y; 1142 } 1143 1144 /++ 1145 $(I Advanced functionality.) 1146 1147 Right margin of the subimage within the source image. 1148 1149 ``` 1150 ╔═══════════╗ 1151 ║ ║ 1152 ║ ┌───┐ ║ 1153 ║ │ S │ ↔ ║ 1154 ║ └───┘ ║ 1155 ║ ║ 1156 ╚═══════════╝ 1157 ``` 1158 +/ 1159 int sourceMarginRight() { 1160 return (sourceMarginX - sourceMarginLeft); 1161 } 1162 1163 /++ 1164 $(I Advanced functionality.) 1165 1166 Bottom margin of the subimage within the source image. 1167 1168 ``` 1169 ╔═══════════╗ 1170 ║ ║ 1171 ║ ┌───┐ ║ 1172 ║ │ S │ ║ 1173 ║ └───┘ ║ 1174 ║ ↕ ║ 1175 ╚═══════════╝ 1176 ``` 1177 +/ 1178 int sourceMarginBottom() { 1179 return (sourceMarginY - sourceMarginTop); 1180 } 1181 1182 /++ 1183 $(I Advanced functionality.) 1184 1185 Left margin of the subimage within the source image. 1186 1187 ``` 1188 ╔═══════════╗ 1189 ║ ║ 1190 ║ ┌───┐ ║ 1191 ║ ↔ │ S │ ║ 1192 ║ └───┘ ║ 1193 ║ ║ 1194 ╚═══════════╝ 1195 ``` 1196 +/ 1197 int sourceMarginLeft() { 1198 return offset.x; 1199 } 1200 } 1201 1202 public const { 1203 /++ 1204 $(I Advanced functionality.) 1205 1206 Calculates the linear offset of the provided point in the subimage 1207 relative to the source image. 1208 +/ 1209 int sourceOffsetOf(Point pos) { 1210 pos = (pos + offset); 1211 return linearOffset(pos, source.width); 1212 } 1213 } 1214 } 1215 1216 /++ 1217 $(I Advanced functionality.) 1218 1219 Wrapper for scanning a [Pixmap] line by line. 1220 +/ 1221 struct PixmapScanner { 1222 private { 1223 const(Pixel)[] _data; 1224 int _width; 1225 } 1226 1227 @safe pure nothrow @nogc: 1228 1229 /// 1230 public this(const(Pixmap) pixmap) { 1231 _data = pixmap.data; 1232 _width = pixmap.width; 1233 } 1234 1235 /// 1236 typeof(this) save() { 1237 return this; 1238 } 1239 1240 /// 1241 bool empty() const { 1242 return (_data.length == 0); 1243 } 1244 1245 /// 1246 const(Pixel)[] front() const { 1247 return _data[0 .. _width]; 1248 } 1249 1250 /// 1251 void popFront() { 1252 _data = _data[_width .. $]; 1253 } 1254 1255 /// 1256 const(Pixel)[] back() const { 1257 return _data[($ - _width) .. $]; 1258 } 1259 1260 /// 1261 void popBack() { 1262 _data = _data[0 .. ($ - _width)]; 1263 } 1264 } 1265 1266 /++ 1267 $(I Advanced functionality.) 1268 1269 Wrapper for scanning a [Pixmap] line by line. 1270 1271 See_also: 1272 Unlike [PixmapScanner], this does not work with `const(Pixmap)`. 1273 +/ 1274 struct PixmapScannerRW { 1275 private { 1276 Pixel[] _data; 1277 int _width; 1278 } 1279 1280 @safe pure nothrow @nogc: 1281 1282 /// 1283 public this(Pixmap pixmap) { 1284 _data = pixmap.data; 1285 _width = pixmap.width; 1286 } 1287 1288 /// 1289 typeof(this) save() { 1290 return this; 1291 } 1292 1293 /// 1294 bool empty() const { 1295 return (_data.length == 0); 1296 } 1297 1298 /// 1299 Pixel[] front() { 1300 return _data[0 .. _width]; 1301 } 1302 1303 /// 1304 void popFront() { 1305 _data = _data[_width .. $]; 1306 } 1307 1308 /// 1309 Pixel[] back() { 1310 return _data[($ - _width) .. $]; 1311 } 1312 1313 /// 1314 void popBack() { 1315 _data = _data[0 .. ($ - _width)]; 1316 } 1317 } 1318 1319 /++ 1320 $(I Advanced functionality.) 1321 1322 Wrapper for scanning a [Pixmap] line by line. 1323 +/ 1324 struct SubPixmapScanner { 1325 private { 1326 const(Pixel)[] _data; 1327 int _width; 1328 int _feed; 1329 } 1330 1331 @safe pure nothrow @nogc: 1332 1333 /// 1334 public this(const(SubPixmap) subPixmap) { 1335 _data = subPixmap.source.data[subPixmap.sourceOffsetLinear .. subPixmap.sourceOffsetLinearEnd]; 1336 _width = subPixmap.size.width; 1337 _feed = subPixmap.source.width; 1338 } 1339 1340 /// 1341 typeof(this) save() { 1342 return this; 1343 } 1344 1345 /// 1346 bool empty() const { 1347 return (_data.length == 0); 1348 } 1349 1350 /// 1351 const(Pixel)[] front() const { 1352 return _data[0 .. _width]; 1353 } 1354 1355 /// 1356 void popFront() { 1357 if (_data.length < _feed) { 1358 _data.length = 0; 1359 return; 1360 } 1361 1362 _data = _data[_feed .. $]; 1363 } 1364 1365 /// 1366 const(Pixel)[] back() const { 1367 return _data[($ - _width) .. $]; 1368 } 1369 1370 /// 1371 void popBack() { 1372 if (_data.length < _feed) { 1373 _data.length = 0; 1374 return; 1375 } 1376 1377 _data = _data[0 .. ($ - _feed)]; 1378 } 1379 } 1380 1381 /++ 1382 $(I Advanced functionality.) 1383 1384 Wrapper for scanning a [Pixmap] line by line. 1385 1386 See_also: 1387 Unlike [SubPixmapScanner], this does not work with `const(SubPixmap)`. 1388 +/ 1389 struct SubPixmapScannerRW { 1390 private { 1391 Pixel[] _data; 1392 int _width; 1393 int _feed; 1394 } 1395 1396 @safe pure nothrow @nogc: 1397 1398 /// 1399 public this(SubPixmap subPixmap) { 1400 _data = subPixmap.source.data[subPixmap.sourceOffsetLinear .. subPixmap.sourceOffsetLinearEnd]; 1401 _width = subPixmap.size.width; 1402 _feed = subPixmap.source.width; 1403 } 1404 1405 /// 1406 typeof(this) save() { 1407 return this; 1408 } 1409 1410 /// 1411 bool empty() const { 1412 return (_data.length == 0); 1413 } 1414 1415 /// 1416 Pixel[] front() { 1417 return _data[0 .. _width]; 1418 } 1419 1420 /// 1421 void popFront() { 1422 if (_data.length < _feed) { 1423 _data.length = 0; 1424 return; 1425 } 1426 1427 _data = _data[_feed .. $]; 1428 } 1429 1430 /// 1431 Pixel[] back() { 1432 return _data[($ - _width) .. $]; 1433 } 1434 1435 /// 1436 void popBack() { 1437 if (_data.length < _feed) { 1438 _data.length = 0; 1439 return; 1440 } 1441 1442 _data = _data[0 .. ($ - _feed)]; 1443 } 1444 } 1445 1446 /// 1447 struct SpriteSheet { 1448 private { 1449 Pixmap _pixmap; 1450 Size _spriteDimensions; 1451 Size _layout; // pre-computed upon construction 1452 } 1453 1454 @safe pure nothrow @nogc: 1455 1456 /// 1457 public this(Pixmap pixmap, Size spriteSize) { 1458 _pixmap = pixmap; 1459 _spriteDimensions = spriteSize; 1460 1461 _layout = Size( 1462 _pixmap.width / _spriteDimensions.width, 1463 _pixmap.height / _spriteDimensions.height, 1464 ); 1465 } 1466 1467 /// 1468 inout(Pixmap) pixmap() inout { 1469 return _pixmap; 1470 } 1471 1472 /// 1473 Size spriteSize() inout { 1474 return _spriteDimensions; 1475 } 1476 1477 /// 1478 Size layout() inout { 1479 return _layout; 1480 } 1481 1482 /// 1483 Point getSpriteColumn(int index) inout { 1484 immutable x = index % layout.width; 1485 immutable y = (index - x) / layout.height; 1486 return Point(x, y); 1487 } 1488 1489 /// 1490 Point getSpritePixelOffset2D(int index) inout { 1491 immutable col = this.getSpriteColumn(index); 1492 return Point( 1493 col.x * _spriteDimensions.width, 1494 col.y * _spriteDimensions.height, 1495 ); 1496 } 1497 } 1498 1499 // Silly micro-optimization 1500 private struct OriginRectangle { 1501 Size size; 1502 1503 @safe pure nothrow @nogc: 1504 1505 int left() const => 0; 1506 int top() const => 0; 1507 int right() const => size.width; 1508 int bottom() const => size.height; 1509 1510 bool intersect(const Rectangle b) const { 1511 // dfmt off 1512 return ( 1513 (b.right > 0 ) && 1514 (b.left < this.right ) && 1515 (b.bottom > 0 ) && 1516 (b.top < this.bottom) 1517 ); 1518 // dfmt on 1519 } 1520 } 1521 1522 @safe pure nothrow: 1523 1524 // misc 1525 private @nogc { 1526 Point pos(Rectangle r) => r.upperLeft; 1527 1528 T max(T)(T a, T b) => (a >= b) ? a : b; 1529 T min(T)(T a, T b) => (a <= b) ? a : b; 1530 } 1531 1532 /++ 1533 Calculates the square root 1534 of an integer number 1535 as an integer number. 1536 +/ 1537 ubyte intSqrt(const ubyte value) @safe pure nothrow @nogc { 1538 switch (value) { 1539 default: 1540 // unreachable 1541 assert(false, "ubyte != uint8"); 1542 case 0: 1543 return 0; 1544 case 1: .. case 2: 1545 return 1; 1546 case 3: .. case 6: 1547 return 2; 1548 case 7: .. case 12: 1549 return 3; 1550 case 13: .. case 20: 1551 return 4; 1552 case 21: .. case 30: 1553 return 5; 1554 case 31: .. case 42: 1555 return 6; 1556 case 43: .. case 56: 1557 return 7; 1558 case 57: .. case 72: 1559 return 8; 1560 case 73: .. case 90: 1561 return 9; 1562 case 91: .. case 110: 1563 return 10; 1564 case 111: .. case 132: 1565 return 11; 1566 case 133: .. case 156: 1567 return 12; 1568 case 157: .. case 182: 1569 return 13; 1570 case 183: .. case 210: 1571 return 14; 1572 case 211: .. case 240: 1573 return 15; 1574 case 241: .. case 255: 1575 return 16; 1576 } 1577 } 1578 1579 /// 1580 unittest { 1581 assert(intSqrt(4) == 2); 1582 assert(intSqrt(9) == 3); 1583 assert(intSqrt(10) == 3); 1584 } 1585 1586 unittest { 1587 import std.math : round, sqrt; 1588 1589 foreach (n; ubyte.min .. ubyte.max + 1) { 1590 ubyte fp = sqrt(float(n)).round().castTo!ubyte; 1591 ubyte i8 = intSqrt(n.castTo!ubyte); 1592 assert(fp == i8); 1593 } 1594 } 1595 1596 /++ 1597 Calculates the square root 1598 of the normalized value 1599 representated by the input integer number. 1600 1601 Normalization: 1602 `[0x00 .. 0xFF]` → `[0.0 .. 1.0]` 1603 1604 Returns: 1605 sqrt(value / 255f) * 255 1606 +/ 1607 ubyte intNormalizedSqrt(const ubyte value) @nogc { 1608 switch (value) { 1609 default: 1610 // unreachable 1611 assert(false, "ubyte != uint8"); 1612 case 0x00: 1613 return 0x00; 1614 case 0x01: 1615 return 0x10; 1616 case 0x02: 1617 return 0x17; 1618 case 0x03: 1619 return 0x1C; 1620 case 0x04: 1621 return 0x20; 1622 case 0x05: 1623 return 0x24; 1624 case 0x06: 1625 return 0x27; 1626 case 0x07: 1627 return 0x2A; 1628 case 0x08: 1629 return 0x2D; 1630 case 0x09: 1631 return 0x30; 1632 case 0x0A: 1633 return 0x32; 1634 case 0x0B: 1635 return 0x35; 1636 case 0x0C: 1637 return 0x37; 1638 case 0x0D: 1639 return 0x3A; 1640 case 0x0E: 1641 return 0x3C; 1642 case 0x0F: 1643 return 0x3E; 1644 case 0x10: 1645 return 0x40; 1646 case 0x11: 1647 return 0x42; 1648 case 0x12: 1649 return 0x44; 1650 case 0x13: 1651 return 0x46; 1652 case 0x14: 1653 return 0x47; 1654 case 0x15: 1655 return 0x49; 1656 case 0x16: 1657 return 0x4B; 1658 case 0x17: 1659 return 0x4D; 1660 case 0x18: 1661 return 0x4E; 1662 case 0x19: 1663 return 0x50; 1664 case 0x1A: 1665 return 0x51; 1666 case 0x1B: 1667 return 0x53; 1668 case 0x1C: 1669 return 0x54; 1670 case 0x1D: 1671 return 0x56; 1672 case 0x1E: 1673 return 0x57; 1674 case 0x1F: 1675 return 0x59; 1676 case 0x20: 1677 return 0x5A; 1678 case 0x21: 1679 return 0x5C; 1680 case 0x22: 1681 return 0x5D; 1682 case 0x23: 1683 return 0x5E; 1684 case 0x24: 1685 return 0x60; 1686 case 0x25: 1687 return 0x61; 1688 case 0x26: 1689 return 0x62; 1690 case 0x27: 1691 return 0x64; 1692 case 0x28: 1693 return 0x65; 1694 case 0x29: 1695 return 0x66; 1696 case 0x2A: 1697 return 0x67; 1698 case 0x2B: 1699 return 0x69; 1700 case 0x2C: 1701 return 0x6A; 1702 case 0x2D: 1703 return 0x6B; 1704 case 0x2E: 1705 return 0x6C; 1706 case 0x2F: 1707 return 0x6D; 1708 case 0x30: 1709 return 0x6F; 1710 case 0x31: 1711 return 0x70; 1712 case 0x32: 1713 return 0x71; 1714 case 0x33: 1715 return 0x72; 1716 case 0x34: 1717 return 0x73; 1718 case 0x35: 1719 return 0x74; 1720 case 0x36: 1721 return 0x75; 1722 case 0x37: 1723 return 0x76; 1724 case 0x38: 1725 return 0x77; 1726 case 0x39: 1727 return 0x79; 1728 case 0x3A: 1729 return 0x7A; 1730 case 0x3B: 1731 return 0x7B; 1732 case 0x3C: 1733 return 0x7C; 1734 case 0x3D: 1735 return 0x7D; 1736 case 0x3E: 1737 return 0x7E; 1738 case 0x3F: 1739 return 0x7F; 1740 case 0x40: 1741 return 0x80; 1742 case 0x41: 1743 return 0x81; 1744 case 0x42: 1745 return 0x82; 1746 case 0x43: 1747 return 0x83; 1748 case 0x44: 1749 return 0x84; 1750 case 0x45: 1751 return 0x85; 1752 case 0x46: 1753 return 0x86; 1754 case 0x47: .. case 0x48: 1755 return 0x87; 1756 case 0x49: 1757 return 0x88; 1758 case 0x4A: 1759 return 0x89; 1760 case 0x4B: 1761 return 0x8A; 1762 case 0x4C: 1763 return 0x8B; 1764 case 0x4D: 1765 return 0x8C; 1766 case 0x4E: 1767 return 0x8D; 1768 case 0x4F: 1769 return 0x8E; 1770 case 0x50: 1771 return 0x8F; 1772 case 0x51: 1773 return 0x90; 1774 case 0x52: .. case 0x53: 1775 return 0x91; 1776 case 0x54: 1777 return 0x92; 1778 case 0x55: 1779 return 0x93; 1780 case 0x56: 1781 return 0x94; 1782 case 0x57: 1783 return 0x95; 1784 case 0x58: 1785 return 0x96; 1786 case 0x59: .. case 0x5A: 1787 return 0x97; 1788 case 0x5B: 1789 return 0x98; 1790 case 0x5C: 1791 return 0x99; 1792 case 0x5D: 1793 return 0x9A; 1794 case 0x5E: 1795 return 0x9B; 1796 case 0x5F: .. case 0x60: 1797 return 0x9C; 1798 case 0x61: 1799 return 0x9D; 1800 case 0x62: 1801 return 0x9E; 1802 case 0x63: 1803 return 0x9F; 1804 case 0x64: .. case 0x65: 1805 return 0xA0; 1806 case 0x66: 1807 return 0xA1; 1808 case 0x67: 1809 return 0xA2; 1810 case 0x68: 1811 return 0xA3; 1812 case 0x69: .. case 0x6A: 1813 return 0xA4; 1814 case 0x6B: 1815 return 0xA5; 1816 case 0x6C: 1817 return 0xA6; 1818 case 0x6D: .. case 0x6E: 1819 return 0xA7; 1820 case 0x6F: 1821 return 0xA8; 1822 case 0x70: 1823 return 0xA9; 1824 case 0x71: .. case 0x72: 1825 return 0xAA; 1826 case 0x73: 1827 return 0xAB; 1828 case 0x74: 1829 return 0xAC; 1830 case 0x75: .. case 0x76: 1831 return 0xAD; 1832 case 0x77: 1833 return 0xAE; 1834 case 0x78: 1835 return 0xAF; 1836 case 0x79: .. case 0x7A: 1837 return 0xB0; 1838 case 0x7B: 1839 return 0xB1; 1840 case 0x7C: 1841 return 0xB2; 1842 case 0x7D: .. case 0x7E: 1843 return 0xB3; 1844 case 0x7F: 1845 return 0xB4; 1846 case 0x80: .. case 0x81: 1847 return 0xB5; 1848 case 0x82: 1849 return 0xB6; 1850 case 0x83: .. case 0x84: 1851 return 0xB7; 1852 case 0x85: 1853 return 0xB8; 1854 case 0x86: 1855 return 0xB9; 1856 case 0x87: .. case 0x88: 1857 return 0xBA; 1858 case 0x89: 1859 return 0xBB; 1860 case 0x8A: .. case 0x8B: 1861 return 0xBC; 1862 case 0x8C: 1863 return 0xBD; 1864 case 0x8D: .. case 0x8E: 1865 return 0xBE; 1866 case 0x8F: 1867 return 0xBF; 1868 case 0x90: .. case 0x91: 1869 return 0xC0; 1870 case 0x92: 1871 return 0xC1; 1872 case 0x93: .. case 0x94: 1873 return 0xC2; 1874 case 0x95: 1875 return 0xC3; 1876 case 0x96: .. case 0x97: 1877 return 0xC4; 1878 case 0x98: 1879 return 0xC5; 1880 case 0x99: .. case 0x9A: 1881 return 0xC6; 1882 case 0x9B: .. case 0x9C: 1883 return 0xC7; 1884 case 0x9D: 1885 return 0xC8; 1886 case 0x9E: .. case 0x9F: 1887 return 0xC9; 1888 case 0xA0: 1889 return 0xCA; 1890 case 0xA1: .. case 0xA2: 1891 return 0xCB; 1892 case 0xA3: .. case 0xA4: 1893 return 0xCC; 1894 case 0xA5: 1895 return 0xCD; 1896 case 0xA6: .. case 0xA7: 1897 return 0xCE; 1898 case 0xA8: 1899 return 0xCF; 1900 case 0xA9: .. case 0xAA: 1901 return 0xD0; 1902 case 0xAB: .. case 0xAC: 1903 return 0xD1; 1904 case 0xAD: 1905 return 0xD2; 1906 case 0xAE: .. case 0xAF: 1907 return 0xD3; 1908 case 0xB0: .. case 0xB1: 1909 return 0xD4; 1910 case 0xB2: 1911 return 0xD5; 1912 case 0xB3: .. case 0xB4: 1913 return 0xD6; 1914 case 0xB5: .. case 0xB6: 1915 return 0xD7; 1916 case 0xB7: 1917 return 0xD8; 1918 case 0xB8: .. case 0xB9: 1919 return 0xD9; 1920 case 0xBA: .. case 0xBB: 1921 return 0xDA; 1922 case 0xBC: 1923 return 0xDB; 1924 case 0xBD: .. case 0xBE: 1925 return 0xDC; 1926 case 0xBF: .. case 0xC0: 1927 return 0xDD; 1928 case 0xC1: .. case 0xC2: 1929 return 0xDE; 1930 case 0xC3: 1931 return 0xDF; 1932 case 0xC4: .. case 0xC5: 1933 return 0xE0; 1934 case 0xC6: .. case 0xC7: 1935 return 0xE1; 1936 case 0xC8: .. case 0xC9: 1937 return 0xE2; 1938 case 0xCA: 1939 return 0xE3; 1940 case 0xCB: .. case 0xCC: 1941 return 0xE4; 1942 case 0xCD: .. case 0xCE: 1943 return 0xE5; 1944 case 0xCF: .. case 0xD0: 1945 return 0xE6; 1946 case 0xD1: .. case 0xD2: 1947 return 0xE7; 1948 case 0xD3: 1949 return 0xE8; 1950 case 0xD4: .. case 0xD5: 1951 return 0xE9; 1952 case 0xD6: .. case 0xD7: 1953 return 0xEA; 1954 case 0xD8: .. case 0xD9: 1955 return 0xEB; 1956 case 0xDA: .. case 0xDB: 1957 return 0xEC; 1958 case 0xDC: .. case 0xDD: 1959 return 0xED; 1960 case 0xDE: .. case 0xDF: 1961 return 0xEE; 1962 case 0xE0: 1963 return 0xEF; 1964 case 0xE1: .. case 0xE2: 1965 return 0xF0; 1966 case 0xE3: .. case 0xE4: 1967 return 0xF1; 1968 case 0xE5: .. case 0xE6: 1969 return 0xF2; 1970 case 0xE7: .. case 0xE8: 1971 return 0xF3; 1972 case 0xE9: .. case 0xEA: 1973 return 0xF4; 1974 case 0xEB: .. case 0xEC: 1975 return 0xF5; 1976 case 0xED: .. case 0xEE: 1977 return 0xF6; 1978 case 0xEF: .. case 0xF0: 1979 return 0xF7; 1980 case 0xF1: .. case 0xF2: 1981 return 0xF8; 1982 case 0xF3: .. case 0xF4: 1983 return 0xF9; 1984 case 0xF5: .. case 0xF6: 1985 return 0xFA; 1986 case 0xF7: .. case 0xF8: 1987 return 0xFB; 1988 case 0xF9: .. case 0xFA: 1989 return 0xFC; 1990 case 0xFB: .. case 0xFC: 1991 return 0xFD; 1992 case 0xFD: .. case 0xFE: 1993 return 0xFE; 1994 case 0xFF: 1995 return 0xFF; 1996 } 1997 } 1998 1999 unittest { 2000 import std.math : round, sqrt; 2001 2002 foreach (n; ubyte.min .. ubyte.max + 1) { 2003 ubyte fp = (sqrt(n / 255.0f) * 255).round().castTo!ubyte; 2004 ubyte i8 = intNormalizedSqrt(n.castTo!ubyte); 2005 assert(fp == i8); 2006 } 2007 } 2008 2009 /++ 2010 Limits a value to a maximum of 0xFF (= 255). 2011 +/ 2012 ubyte clamp255(Tint)(const Tint value) @nogc { 2013 pragma(inline, true); 2014 return (value < 0xFF) ? value.castTo!ubyte : 0xFF; 2015 } 2016 2017 /++ 2018 Fast 8-bit “percentage” function 2019 2020 This function optimizes its runtime performance by substituting 2021 the division by 255 with an approximation using bitshifts. 2022 2023 Nonetheless, its result are as accurate as a floating point 2024 division with 64-bit precision. 2025 2026 Params: 2027 nPercentage = percentage as the number of 255ths (“two hundred fifty-fifths”) 2028 value = base value (“total”) 2029 2030 Returns: 2031 `round(value * nPercentage / 255.0)` 2032 +/ 2033 ubyte n255thsOf(const ubyte nPercentage, const ubyte value) @nogc { 2034 immutable factor = (nPercentage | (nPercentage << 8)); 2035 return (((value * factor) + 0x8080) >> 16); 2036 } 2037 2038 @safe unittest { 2039 // Accuracy verification 2040 2041 static ubyte n255thsOfFP64(const ubyte nPercentage, const ubyte value) { 2042 return (double(value) * double(nPercentage) / 255.0).round().castTo!ubyte(); 2043 } 2044 2045 for (int value = ubyte.min; value <= ubyte.max; ++value) { 2046 for (int percent = ubyte.min; percent <= ubyte.max; ++percent) { 2047 immutable v = cast(ubyte) value; 2048 immutable p = cast(ubyte) percent; 2049 2050 immutable approximated = n255thsOf(p, v); 2051 immutable precise = n255thsOfFP64(p, v); 2052 assert(approximated == precise); 2053 } 2054 } 2055 } 2056 2057 /// 2058 ubyte percentageDecimalToUInt8(const float decimal) @nogc 2059 in (decimal >= 0) 2060 in (decimal <= 1) { 2061 return round(decimal * 255).castTo!ubyte; 2062 } 2063 2064 /// 2065 float percentageUInt8ToDecimal(const ubyte n255ths) @nogc { 2066 return (float(n255ths) / 255.0f); 2067 } 2068 2069 // ==== Image manipulation functions ==== 2070 2071 /++ 2072 Lowers the opacity of a Pixel. 2073 2074 This function multiplies the opacity of the input 2075 with the given percentage. 2076 2077 See_Also: 2078 Use [decreaseOpacityF] with decimal opacity values in percent (%). 2079 +/ 2080 Pixel decreaseOpacity(const Pixel source, ubyte opacityPercentage) @nogc { 2081 return Pixel( 2082 source.r, 2083 source.g, 2084 source.b, 2085 opacityPercentage.n255thsOf(source.a), 2086 ); 2087 } 2088 2089 /++ 2090 Lowers the opacity of a Pixel. 2091 2092 This function multiplies the opacity of the input 2093 with the given percentage. 2094 2095 Value Range: 2096 0.0 = 0% 2097 1.0 = 100% 2098 2099 See_Also: 2100 Use [opacity] with 8-bit integer opacity values (in 255ths). 2101 +/ 2102 Pixel decreaseOpacityF(const Pixel source, float opacityPercentage) @nogc { 2103 return decreaseOpacity(source, percentageDecimalToUInt8(opacityPercentage)); 2104 } 2105 2106 // Don’t get fooled by the name of this function. 2107 // It’s called like that for consistency reasons. 2108 private void decreaseOpacityInto(const Pixmap source, Pixmap target, ubyte opacityPercentage) @trusted @nogc { 2109 debug assert(source.data.length == target.data.length); 2110 foreach (idx, ref px; target.data) { 2111 px = decreaseOpacity(source.data.ptr[idx], opacityPercentage); 2112 } 2113 } 2114 2115 /++ 2116 Lowers the opacity of a [Pixmap]. 2117 2118 This operation updates the alpha-channel value of each pixel. 2119 → `alpha *= opacity` 2120 2121 See_Also: 2122 Use [decreaseOpacityF] with decimal opacity values in percent (%). 2123 +/ 2124 Pixmap decreaseOpacity(const Pixmap source, Pixmap target, ubyte opacityPercentage) @nogc { 2125 target.adjustTo(source.decreaseOpacityCalcDims()); 2126 source.decreaseOpacityInto(target, opacityPercentage); 2127 return target; 2128 } 2129 2130 /// ditto 2131 Pixmap decreaseOpacityNew(const Pixmap source, ubyte opacityPercentage) { 2132 auto target = Pixmap.makeNew(source.decreaseOpacityCalcDims()); 2133 source.decreaseOpacityInto(target, opacityPercentage); 2134 return target; 2135 } 2136 2137 /// ditto 2138 void decreaseOpacityInPlace(Pixmap source, ubyte opacityPercentage) @nogc { 2139 foreach (ref px; source.data) { 2140 px.a = opacityPercentage.n255thsOf(px.a); 2141 } 2142 } 2143 2144 /// ditto 2145 PixmapBlueprint decreaseOpacityCalcDims(const Pixmap source) @nogc { 2146 return PixmapBlueprint.fromPixmap(source); 2147 } 2148 2149 /++ 2150 Adjusts the opacity of a [Pixmap]. 2151 2152 This operation updates the alpha-channel value of each pixel. 2153 → `alpha *= opacity` 2154 2155 See_Also: 2156 Use [decreaseOpacity] with 8-bit integer opacity values (in 255ths). 2157 +/ 2158 Pixmap decreaseOpacityF(const Pixmap source, Pixmap target, float opacityPercentage) @nogc { 2159 return source.decreaseOpacity(target, percentageDecimalToUInt8(opacityPercentage)); 2160 } 2161 2162 /// ditto 2163 Pixmap decreaseOpacityFNew(const Pixmap source, float opacityPercentage) { 2164 return source.decreaseOpacityNew(percentageDecimalToUInt8(opacityPercentage)); 2165 } 2166 2167 /// ditto 2168 void decreaseOpacityFInPlace(Pixmap source, float opacityPercentage) @nogc { 2169 return source.decreaseOpacityInPlace(percentageDecimalToUInt8(opacityPercentage)); 2170 } 2171 2172 /// ditto 2173 PixmapBlueprint decreaseOpacityF(Pixmap source) @nogc { 2174 return PixmapBlueprint.fromPixmap(source); 2175 } 2176 2177 /++ 2178 Inverts a color (to its negative color). 2179 +/ 2180 Pixel invert(const Pixel color) @nogc { 2181 return Pixel( 2182 0xFF - color.r, 2183 0xFF - color.g, 2184 0xFF - color.b, 2185 color.a, 2186 ); 2187 } 2188 2189 private void invertInto(const Pixmap source, Pixmap target) @trusted @nogc { 2190 debug assert(source.length == target.length); 2191 foreach (idx, ref px; target.data) { 2192 px = invert(source.data.ptr[idx]); 2193 } 2194 } 2195 2196 /++ 2197 Inverts all colors to produce a $(I negative image). 2198 2199 $(TIP 2200 Develops a positive image when applied to a negative one. 2201 ) 2202 +/ 2203 Pixmap invert(const Pixmap source, Pixmap target) @nogc { 2204 target.adjustTo(source.invertCalcDims()); 2205 source.invertInto(target); 2206 return target; 2207 } 2208 2209 /// ditto 2210 Pixmap invertNew(const Pixmap source) { 2211 auto target = Pixmap.makeNew(source.invertCalcDims()); 2212 source.invertInto(target); 2213 return target; 2214 } 2215 2216 /// ditto 2217 void invertInPlace(Pixmap pixmap) @nogc { 2218 foreach (ref px; pixmap.data) { 2219 px = invert(px); 2220 } 2221 } 2222 2223 /// ditto 2224 PixmapBlueprint invertCalcDims(const Pixmap source) @nogc { 2225 return PixmapBlueprint.fromPixmap(source); 2226 } 2227 2228 /++ 2229 Crops an image and stores the result in the provided target Pixmap. 2230 2231 The size of the area to crop the image to 2232 is derived from the size of the target. 2233 2234 --- 2235 // This function can be used to omit a redundant size parameter 2236 // in cases like this: 2237 target = crop(source, target, target.size, offset); 2238 2239 // → Instead do: 2240 cropTo(source, target, offset); 2241 --- 2242 +/ 2243 void cropTo(const Pixmap source, Pixmap target, Point offset = Point(0, 0)) @nogc { 2244 auto src = const(SubPixmap)(source, target.size, offset); 2245 src.extractToPixmapCopyImpl(target); 2246 } 2247 2248 // consistency 2249 private alias cropInto = cropTo; 2250 2251 /++ 2252 Crops an image to the provided size with the requested offset. 2253 2254 The target Pixmap must be big enough in length to hold the cropped image. 2255 +/ 2256 Pixmap crop(const Pixmap source, Pixmap target, Size cropToSize, Point offset = Point(0, 0)) @nogc { 2257 target.adjustTo(cropCalcDims(cropToSize)); 2258 cropInto(source, target, offset); 2259 return target; 2260 } 2261 2262 /// ditto 2263 Pixmap cropNew(const Pixmap source, Size cropToSize, Point offset = Point(0, 0)) { 2264 auto target = Pixmap.makeNew(cropToSize); 2265 cropInto(source, target, offset); 2266 return target; 2267 } 2268 2269 /// ditto 2270 Pixmap cropInPlace(Pixmap source, Size cropToSize, Point offset = Point(0, 0)) @nogc { 2271 Pixmap target = source; 2272 target.width = cropToSize.width; 2273 target.data = target.data[0 .. cropToSize.area]; 2274 2275 auto src = const(SubPixmap)(source, cropToSize, offset); 2276 src.extractToPixmapCopyPixelByPixelImpl(target); 2277 return target; 2278 } 2279 2280 /// ditto 2281 PixmapBlueprint cropCalcDims(Size cropToSize) @nogc { 2282 return PixmapBlueprint.fromSize(cropToSize); 2283 } 2284 2285 private void transposeInto(const Pixmap source, Pixmap target) @nogc { 2286 foreach (y; 0 .. target.width) { 2287 foreach (x; 0 .. source.width) { 2288 const idxSrc = linearOffset(Point(x, y), source.width); 2289 const idxDst = linearOffset(Point(y, x), target.width); 2290 2291 target.data[idxDst] = source.data[idxSrc]; 2292 } 2293 } 2294 } 2295 2296 /++ 2297 Transposes an image. 2298 2299 ``` 2300 ╔══╗ ╔══╗ 2301 ║# ║ ║#+║ 2302 ║+x║ → ║ x║ 2303 ╚══╝ ╚══╝ 2304 ``` 2305 +/ 2306 Pixmap transpose(const Pixmap source, Pixmap target) @nogc { 2307 target.adjustTo(source.transposeCalcDims()); 2308 source.transposeInto(target); 2309 return target; 2310 } 2311 2312 /// ditto 2313 Pixmap transposeNew(const Pixmap source) { 2314 auto target = Pixmap.makeNew(source.transposeCalcDims()); 2315 source.transposeInto(target); 2316 return target; 2317 } 2318 2319 /// ditto 2320 PixmapBlueprint transposeCalcDims(const Pixmap source) @nogc { 2321 return PixmapBlueprint(source.length, source.height); 2322 } 2323 2324 private void rotateClockwiseInto(const Pixmap source, Pixmap target) @nogc { 2325 const area = source.data.length; 2326 const rowLength = source.size.height; 2327 ptrdiff_t cursor = -1; 2328 2329 foreach (px; source.data) { 2330 cursor += rowLength; 2331 if (cursor > area) { 2332 cursor -= (area + 1); 2333 } 2334 2335 target.data[cursor] = px; 2336 } 2337 } 2338 2339 /++ 2340 Rotates an image by 90° clockwise. 2341 2342 ``` 2343 ╔══╗ ╔══╗ 2344 ║# ║ ║+#║ 2345 ║+x║ → ║x ║ 2346 ╚══╝ ╚══╝ 2347 ``` 2348 +/ 2349 Pixmap rotateClockwise(const Pixmap source, Pixmap target) @nogc { 2350 target.adjustTo(source.rotateClockwiseCalcDims()); 2351 source.rotateClockwiseInto(target); 2352 return target; 2353 } 2354 2355 /// ditto 2356 Pixmap rotateClockwiseNew(const Pixmap source) { 2357 auto target = Pixmap.makeNew(source.rotateClockwiseCalcDims()); 2358 source.rotateClockwiseInto(target); 2359 return target; 2360 } 2361 2362 /// ditto 2363 PixmapBlueprint rotateClockwiseCalcDims(const Pixmap source) @nogc { 2364 return PixmapBlueprint(source.length, source.height); 2365 } 2366 2367 private void rotateCounterClockwiseInto(const Pixmap source, Pixmap target) @nogc { 2368 // TODO: can this be optimized? 2369 target = transpose(source, target); 2370 target.flipVerticallyInPlace(); 2371 } 2372 2373 /++ 2374 Rotates an image by 90° counter-clockwise. 2375 2376 ``` 2377 ╔══╗ ╔══╗ 2378 ║# ║ ║ x║ 2379 ║+x║ → ║#+║ 2380 ╚══╝ ╚══╝ 2381 ``` 2382 +/ 2383 Pixmap rotateCounterClockwise(const Pixmap source, Pixmap target) @nogc { 2384 target.adjustTo(source.rotateCounterClockwiseCalcDims()); 2385 source.rotateCounterClockwiseInto(target); 2386 return target; 2387 } 2388 2389 /// ditto 2390 Pixmap rotateCounterClockwiseNew(const Pixmap source) { 2391 auto target = Pixmap.makeNew(source.rotateCounterClockwiseCalcDims()); 2392 source.rotateCounterClockwiseInto(target); 2393 return target; 2394 } 2395 2396 /// ditto 2397 PixmapBlueprint rotateCounterClockwiseCalcDims(const Pixmap source) @nogc { 2398 return PixmapBlueprint(source.length, source.height); 2399 } 2400 2401 private void rotate180degInto(const Pixmap source, Pixmap target) @nogc { 2402 // Technically, this is implemented as flip vertical + flip horizontal. 2403 auto src = PixmapScanner(source); 2404 auto dst = PixmapScannerRW(target); 2405 2406 foreach (srcLine; src) { 2407 auto dstLine = dst.back; 2408 foreach (idxSrc, px; srcLine) { 2409 const idxDst = (dstLine.length - (idxSrc + 1)); 2410 dstLine[idxDst] = px; 2411 } 2412 dst.popBack(); 2413 } 2414 } 2415 2416 /++ 2417 Rotates an image by 180°. 2418 2419 ``` 2420 ╔═══╗ ╔═══╗ 2421 ║#- ║ ║%~~║ 2422 ║~~%║ → ║ -#║ 2423 ╚═══╝ ╚═══╝ 2424 ``` 2425 +/ 2426 Pixmap rotate180deg(const Pixmap source, Pixmap target) @nogc { 2427 target.adjustTo(source.rotate180degCalcDims()); 2428 source.rotate180degInto(target); 2429 return target; 2430 } 2431 2432 /// ditto 2433 Pixmap rotate180degNew(const Pixmap source) { 2434 auto target = Pixmap.makeNew(source.size); 2435 source.rotate180degInto(target); 2436 return target; 2437 } 2438 2439 /// ditto 2440 void rotate180degInPlace(Pixmap source) @nogc { 2441 auto scanner = PixmapScannerRW(source); 2442 2443 // Technically, this is implemented as a flip vertical + flip horizontal 2444 // combo, i.e. the image is flipped vertically line by line, but the lines 2445 // are overwritten in a horizontally flipped way. 2446 while (!scanner.empty) { 2447 auto a = scanner.front; 2448 auto b = scanner.back; 2449 2450 // middle line? (odd number of lines) 2451 if (a.ptr is b.ptr) { 2452 break; 2453 } 2454 2455 foreach (idxSrc, ref pxA; a) { 2456 const idxDst = (b.length - (idxSrc + 1)); 2457 const tmp = pxA; 2458 pxA = b[idxDst]; 2459 b[idxDst] = tmp; 2460 } 2461 2462 scanner.popFront(); 2463 scanner.popBack(); 2464 } 2465 } 2466 2467 /// 2468 PixmapBlueprint rotate180degCalcDims(const Pixmap source) @nogc { 2469 return PixmapBlueprint.fromPixmap(source); 2470 } 2471 2472 private void flipHorizontallyInto(const Pixmap source, Pixmap target) @nogc { 2473 auto src = PixmapScanner(source); 2474 auto dst = PixmapScannerRW(target); 2475 2476 foreach (srcLine; src) { 2477 auto dstLine = dst.front; 2478 foreach (idxSrc, px; srcLine) { 2479 const idxDst = (dstLine.length - (idxSrc + 1)); 2480 dstLine[idxDst] = px; 2481 } 2482 2483 dst.popFront(); 2484 } 2485 } 2486 2487 /++ 2488 Flips an image horizontally. 2489 2490 ``` 2491 ╔═══╗ ╔═══╗ 2492 ║#-.║ → ║.-#║ 2493 ╚═══╝ ╚═══╝ 2494 ``` 2495 +/ 2496 Pixmap flipHorizontally(const Pixmap source, Pixmap target) @nogc { 2497 target.adjustTo(source.flipHorizontallyCalcDims()); 2498 source.flipHorizontallyInto(target); 2499 return target; 2500 } 2501 2502 /// ditto 2503 Pixmap flipHorizontallyNew(const Pixmap source) { 2504 auto target = Pixmap.makeNew(source.size); 2505 source.flipHorizontallyInto(target); 2506 return target; 2507 } 2508 2509 /// ditto 2510 void flipHorizontallyInPlace(Pixmap source) @nogc { 2511 auto scanner = PixmapScannerRW(source); 2512 2513 foreach (line; scanner) { 2514 const idxMiddle = (1 + (line.length >> 1)); 2515 auto halfA = line[0 .. idxMiddle]; 2516 2517 foreach (idxA, ref px; halfA) { 2518 const idxB = (line.length - (idxA + 1)); 2519 const tmp = line[idxB]; 2520 // swap 2521 line[idxB] = px; 2522 px = tmp; 2523 } 2524 } 2525 } 2526 2527 /// ditto 2528 PixmapBlueprint flipHorizontallyCalcDims(const Pixmap source) @nogc { 2529 return PixmapBlueprint.fromPixmap(source); 2530 } 2531 2532 private void flipVerticallyInto(const Pixmap source, Pixmap target) @nogc { 2533 auto src = PixmapScanner(source); 2534 auto dst = PixmapScannerRW(target); 2535 2536 foreach (srcLine; src) { 2537 dst.back[] = srcLine[]; 2538 dst.popBack(); 2539 } 2540 } 2541 2542 /++ 2543 Flips an image vertically. 2544 2545 ``` 2546 ╔═══╗ ╔═══╗ 2547 ║## ║ ║ -║ 2548 ║ -║ → ║## ║ 2549 ╚═══╝ ╚═══╝ 2550 ``` 2551 +/ 2552 Pixmap flipVertically(const Pixmap source, Pixmap target) @nogc { 2553 target.adjustTo(source.flipVerticallyCalcDims()); 2554 flipVerticallyInto(source, target); 2555 return target; 2556 } 2557 2558 /// ditto 2559 Pixmap flipVerticallyNew(const Pixmap source) { 2560 auto target = Pixmap.makeNew(source.flipVerticallyCalcDims()); 2561 source.flipVerticallyInto(target); 2562 return target; 2563 } 2564 2565 /// ditto 2566 void flipVerticallyInPlace(Pixmap source) @nogc { 2567 auto scanner = PixmapScannerRW(source); 2568 2569 while (!scanner.empty) { 2570 auto a = scanner.front; 2571 auto b = scanner.back; 2572 2573 // middle line? (odd number of lines) 2574 if (a.ptr is b.ptr) { 2575 break; 2576 } 2577 2578 foreach (idx, ref pxA; a) { 2579 const tmp = pxA; 2580 pxA = b[idx]; 2581 b[idx] = tmp; 2582 } 2583 2584 scanner.popFront(); 2585 scanner.popBack(); 2586 } 2587 } 2588 2589 /// ditto 2590 PixmapBlueprint flipVerticallyCalcDims(const Pixmap source) @nogc { 2591 return PixmapBlueprint.fromPixmap(source); 2592 } 2593 2594 @safe pure nothrow @nogc: 2595 2596 // ==== Blending functions ==== 2597 2598 /++ 2599 Alpha-blending accuracy level 2600 2601 $(TIP 2602 This primarily exists for performance reasons. 2603 In my tests LLVM manages to auto-vectorize the RGB-only codepath significantly better, 2604 while the codegen for the accurate RGBA path is pretty conservative. 2605 2606 This provides an optimization opportunity for use-cases 2607 that don’t require an alpha-channel on the result. 2608 ) 2609 +/ 2610 enum BlendAccuracy { 2611 /++ 2612 Only RGB channels will have the correct result. 2613 2614 A(lpha) channel can contain any value. 2615 2616 Suitable for blending into non-transparent targets (e.g. framebuffer, canvas) 2617 where the resulting alpha-channel (opacity) value does not matter. 2618 +/ 2619 rgb = false, 2620 2621 /++ 2622 All RGBA channels will have the correct result. 2623 2624 Suitable for blending into transparent targets (e.g. images) 2625 where the resulting alpha-channel (opacity) value matters. 2626 2627 Use this mode for image manipulation. 2628 +/ 2629 rgba = true, 2630 } 2631 2632 /++ 2633 Blend modes 2634 2635 $(NOTE 2636 As blending operations are implemented as integer calculations, 2637 results may be slightly less precise than those from image manipulation 2638 programs using floating-point math. 2639 ) 2640 2641 See_Also: 2642 <https://www.w3.org/TR/compositing/#blending> 2643 +/ 2644 enum BlendMode { 2645 /// 2646 none = 0, 2647 /// 2648 replace = none, 2649 /// 2650 normal = 1, 2651 /// 2652 alpha = normal, 2653 2654 /// 2655 multiply, 2656 /// 2657 screen, 2658 2659 /// 2660 overlay, 2661 /// 2662 hardLight, 2663 /// 2664 softLight, 2665 2666 /// 2667 darken, 2668 /// 2669 lighten, 2670 2671 /// 2672 colorDodge, 2673 /// 2674 colorBurn, 2675 2676 /// 2677 difference, 2678 /// 2679 exclusion, 2680 /// 2681 subtract, 2682 /// 2683 divide, 2684 } 2685 2686 /// 2687 alias Blend = BlendMode; 2688 2689 // undocumented 2690 enum blendNormal = BlendMode.normal; 2691 2692 /// 2693 alias BlendFn = ubyte function(const ubyte background, const ubyte foreground) pure nothrow @nogc; 2694 2695 /++ 2696 Blends `source` into `target` 2697 with respect to the opacity of the source image (as stored in the alpha channel). 2698 2699 See_Also: 2700 [alphaBlendRGBA] and [alphaBlendRGB] are shorthand functions 2701 in cases where no special blending algorithm is needed. 2702 +/ 2703 template alphaBlend(BlendFn blend = null, BlendAccuracy accuracy = BlendAccuracy.rgba) { 2704 /// ditto 2705 public void alphaBlend(scope Pixel[] target, scope const Pixel[] source) @trusted 2706 in (source.length == target.length) { 2707 foreach (immutable idx, ref pxTarget; target) { 2708 alphaBlend(pxTarget, source.ptr[idx]); 2709 } 2710 } 2711 2712 /// ditto 2713 public void alphaBlend(ref Pixel pxTarget, const Pixel pxSource) @trusted { 2714 pragma(inline, true); 2715 2716 static if (accuracy == BlendAccuracy.rgba) { 2717 immutable alphaResult = clamp255(pxSource.a + n255thsOf(pxTarget.a, (0xFF - pxSource.a))); 2718 //immutable alphaResult = clamp255(pxTarget.a + n255thsOf(pxSource.a, (0xFF - pxTarget.a))); 2719 } 2720 2721 immutable alphaSource = (pxSource.a | (pxSource.a << 8)); 2722 immutable alphaTarget = (0xFFFF - alphaSource); 2723 2724 foreach (immutable ib, ref px; pxTarget.components) { 2725 static if (blend !is null) { 2726 immutable bx = blend(px, pxSource.components.ptr[ib]); 2727 } else { 2728 immutable bx = pxSource.components.ptr[ib]; 2729 } 2730 immutable d = cast(ubyte)(((px * alphaTarget) + 0x8080) >> 16); 2731 immutable s = cast(ubyte)(((bx * alphaSource) + 0x8080) >> 16); 2732 px = cast(ubyte)(d + s); 2733 } 2734 2735 static if (accuracy == BlendAccuracy.rgba) { 2736 pxTarget.a = alphaResult; 2737 } 2738 } 2739 } 2740 2741 /// ditto 2742 template alphaBlend(BlendAccuracy accuracy, BlendFn blend = null) { 2743 alias alphaBlend = alphaBlend!(blend, accuracy); 2744 } 2745 2746 /++ 2747 Blends `source` into `target` 2748 with respect to the opacity of the source image (as stored in the alpha channel). 2749 2750 This variant is $(slower than) [alphaBlendRGB], 2751 but calculates the correct alpha-channel value of the target. 2752 See [BlendAccuracy] for further explanation. 2753 +/ 2754 public void alphaBlendRGBA(scope Pixel[] target, scope const Pixel[] source) @safe { 2755 return alphaBlend!(null, BlendAccuracy.rgba)(target, source); 2756 } 2757 2758 /// ditto 2759 public void alphaBlendRGBA(ref Pixel pxTarget, const Pixel pxSource) @safe { 2760 return alphaBlend!(null, BlendAccuracy.rgba)(pxTarget, pxSource); 2761 } 2762 2763 /++ 2764 Blends `source` into `target` 2765 with respect to the opacity of the source image (as stored in the alpha channel). 2766 2767 This variant is $(B faster than) [alphaBlendRGBA], 2768 but leads to a wrong alpha-channel value in the target. 2769 Useful because of the performance advantage in cases where the resulting 2770 alpha does not matter. 2771 See [BlendAccuracy] for further explanation. 2772 +/ 2773 public void alphaBlendRGB(scope Pixel[] target, scope const Pixel[] source) @safe { 2774 return alphaBlend!(null, BlendAccuracy.rgb)(target, source); 2775 } 2776 2777 /// ditto 2778 public void alphaBlendRGB(ref Pixel pxTarget, const Pixel pxSource) @safe { 2779 return alphaBlend!(null, BlendAccuracy.rgb)(pxTarget, pxSource); 2780 } 2781 2782 /++ 2783 Blends pixel `source` into pixel `target` 2784 using the requested [BlendMode|blending mode]. 2785 +/ 2786 template blendPixel(BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba) { 2787 2788 static if (mode == BlendMode.replace) { 2789 /// ditto 2790 void blendPixel(ref Pixel target, const Pixel source) { 2791 target = source; 2792 } 2793 } 2794 2795 static if (mode == BlendMode.alpha) { 2796 /// ditto 2797 void blendPixel(ref Pixel target, const Pixel source) { 2798 return alphaBlend!accuracy(target, source); 2799 } 2800 } 2801 2802 static if (mode == BlendMode.multiply) { 2803 /// ditto 2804 void blendPixel(ref Pixel target, const Pixel source) { 2805 return alphaBlend!(accuracy, 2806 (a, b) => n255thsOf(a, b) 2807 )(target, source); 2808 } 2809 } 2810 2811 static if (mode == BlendMode.screen) { 2812 /// ditto 2813 void blendPixel()(ref Pixel target, const Pixel source) { 2814 return alphaBlend!(accuracy, 2815 (a, b) => castTo!ubyte(0xFF - n255thsOf((0xFF - a), (0xFF - b))) 2816 )(target, source); 2817 } 2818 } 2819 2820 static if (mode == BlendMode.darken) { 2821 /// ditto 2822 void blendPixel()(ref Pixel target, const Pixel source) { 2823 return alphaBlend!(accuracy, 2824 (a, b) => min(a, b) 2825 )(target, source); 2826 } 2827 } 2828 static if (mode == BlendMode.lighten) { 2829 /// ditto 2830 void blendPixel()(ref Pixel target, const Pixel source) { 2831 return alphaBlend!(accuracy, 2832 (a, b) => max(a, b) 2833 )(target, source); 2834 } 2835 } 2836 2837 static if (mode == BlendMode.overlay) { 2838 /// ditto 2839 void blendPixel()(ref Pixel target, const Pixel source) { 2840 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 2841 if (b < 0x80) { 2842 return n255thsOf((2 * b).castTo!ubyte, f); 2843 } 2844 return castTo!ubyte( 2845 0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - b)), (0xFF - f)) 2846 ); 2847 })(target, source); 2848 } 2849 } 2850 2851 static if (mode == BlendMode.hardLight) { 2852 /// ditto 2853 void blendPixel()(ref Pixel target, const Pixel source) { 2854 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 2855 if (f < 0x80) { 2856 return n255thsOf(castTo!ubyte(2 * f), b); 2857 } 2858 return castTo!ubyte( 2859 0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - f)), (0xFF - b)) 2860 ); 2861 })(target, source); 2862 } 2863 } 2864 2865 static if (mode == BlendMode.softLight) { 2866 /// ditto 2867 void blendPixel()(ref Pixel target, const Pixel source) { 2868 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 2869 if (f < 0x80) { 2870 // dfmt off 2871 return castTo!ubyte( 2872 b - n255thsOf( 2873 n255thsOf((0xFF - 2 * f).castTo!ubyte, b), 2874 (0xFF - b), 2875 ) 2876 ); 2877 // dfmt on 2878 } 2879 2880 // TODO: optimize if possible 2881 // dfmt off 2882 immutable ubyte d = (b < 0x40) 2883 ? castTo!ubyte((b * (0x3FC + (((16 * b - 0xBF4) * b) / 255))) / 255) 2884 : intNormalizedSqrt(b); 2885 //dfmt on 2886 2887 return castTo!ubyte( 2888 b + n255thsOf((2 * f - 0xFF).castTo!ubyte, (d - b).castTo!ubyte) 2889 ); 2890 })(target, source); 2891 } 2892 } 2893 2894 static if (mode == BlendMode.colorDodge) { 2895 /// ditto 2896 void blendPixel()(ref Pixel target, const Pixel source) { 2897 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 2898 if (b == 0x00) { 2899 return ubyte(0x00); 2900 } 2901 if (f == 0xFF) { 2902 return ubyte(0xFF); 2903 } 2904 return min( 2905 ubyte(0xFF), 2906 clamp255((255 * b) / (0xFF - f)) 2907 ); 2908 })(target, source); 2909 } 2910 } 2911 2912 static if (mode == BlendMode.colorBurn) { 2913 /// ditto 2914 void blendPixel()(ref Pixel target, const Pixel source) { 2915 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 2916 if (b == 0xFF) { 2917 return ubyte(0xFF); 2918 } 2919 if (f == 0x00) { 2920 return ubyte(0x00); 2921 } 2922 2923 immutable m = min( 2924 ubyte(0xFF), 2925 clamp255(((0xFF - b) * 255) / f) 2926 ); 2927 return castTo!ubyte(0xFF - m); 2928 })(target, source); 2929 } 2930 } 2931 2932 static if (mode == BlendMode.difference) { 2933 /// ditto 2934 void blendPixel()(ref Pixel target, const Pixel source) { 2935 return alphaBlend!(accuracy, 2936 (b, f) => (b > f) ? castTo!ubyte(b - f) : castTo!ubyte(f - b) 2937 )(target, source); 2938 } 2939 } 2940 2941 static if (mode == BlendMode.exclusion) { 2942 /// ditto 2943 void blendPixel()(ref Pixel target, const Pixel source) { 2944 return alphaBlend!(accuracy, 2945 (b, f) => castTo!ubyte(b + f - (2 * n255thsOf(f, b))) 2946 )(target, source); 2947 } 2948 } 2949 2950 static if (mode == BlendMode.subtract) { 2951 /// ditto 2952 void blendPixel()(ref Pixel target, const Pixel source) { 2953 return alphaBlend!(accuracy, 2954 (b, f) => (b > f) ? castTo!ubyte(b - f) : ubyte(0) 2955 )(target, source); 2956 } 2957 } 2958 2959 static if (mode == BlendMode.divide) { 2960 /// ditto 2961 void blendPixel()(ref Pixel target, const Pixel source) { 2962 return alphaBlend!(accuracy, 2963 (b, f) => (f == 0) ? ubyte(0xFF) : clamp255(0xFF * b / f) 2964 )(target, source); 2965 } 2966 } 2967 2968 //else { 2969 // static assert(false, "Missing `blendPixel()` implementation for `BlendMode`.`" ~ mode ~ "`."); 2970 //} 2971 } 2972 2973 /++ 2974 Blends the pixel data of `source` into `target` 2975 using the requested [BlendMode|blending mode]. 2976 2977 `source` and `target` MUST have the same length. 2978 +/ 2979 void blendPixels( 2980 BlendMode mode, 2981 BlendAccuracy accuracy, 2982 )(scope Pixel[] target, scope const Pixel[] source) @trusted 2983 in (source.length == target.length) { 2984 static if (mode == BlendMode.replace) { 2985 // explicit optimization 2986 target.ptr[0 .. target.length] = source.ptr[0 .. target.length]; 2987 } else { 2988 2989 // better error message in case it’s not implemented 2990 static if (!is(typeof(blendPixel!(mode, accuracy)))) { 2991 pragma(msg, "Hint: Missing or bad `blendPixel!(" ~ mode.stringof ~ ")`."); 2992 } 2993 2994 foreach (immutable idx, ref pxTarget; target) { 2995 blendPixel!(mode, accuracy)(pxTarget, source.ptr[idx]); 2996 } 2997 } 2998 } 2999 3000 /// ditto 3001 void blendPixels(BlendAccuracy accuracy)(scope Pixel[] target, scope const Pixel[] source, BlendMode mode) { 3002 import std.meta : NoDuplicates; 3003 import std.traits : EnumMembers; 3004 3005 final switch (mode) with (BlendMode) { 3006 static foreach (m; NoDuplicates!(EnumMembers!BlendMode)) { 3007 case m: 3008 return blendPixels!(m, accuracy)(target, source); 3009 } 3010 } 3011 } 3012 3013 /// ditto 3014 void blendPixels( 3015 scope Pixel[] target, 3016 scope const Pixel[] source, 3017 BlendMode mode, 3018 BlendAccuracy accuracy = BlendAccuracy.rgba, 3019 ) { 3020 if (accuracy == BlendAccuracy.rgb) { 3021 return blendPixels!(BlendAccuracy.rgb)(target, source, mode); 3022 } else { 3023 return blendPixels!(BlendAccuracy.rgba)(target, source, mode); 3024 } 3025 } 3026 3027 // ==== Drawing functions ==== 3028 3029 /++ 3030 Draws a single pixel 3031 +/ 3032 void drawPixel(Pixmap target, Point pos, Pixel color) { 3033 immutable size_t offset = linearOffset(target.width, pos); 3034 target.data[offset] = color; 3035 } 3036 3037 /++ 3038 Draws a rectangle 3039 +/ 3040 void drawRectangle(Pixmap target, Rectangle rectangle, Pixel color) { 3041 alias r = rectangle; 3042 3043 immutable tRect = OriginRectangle( 3044 Size(target.width, target.height), 3045 ); 3046 3047 // out of bounds? 3048 if (!tRect.intersect(r)) { 3049 return; 3050 } 3051 3052 immutable drawingTarget = Point( 3053 (r.pos.x >= 0) ? r.pos.x : 0, 3054 (r.pos.y >= 0) ? r.pos.y : 0, 3055 ); 3056 3057 immutable drawingEnd = Point( 3058 (r.right < tRect.right) ? r.right : tRect.right, 3059 (r.bottom < tRect.bottom) ? r.bottom : tRect.bottom, 3060 ); 3061 3062 immutable int drawingWidth = drawingEnd.x - drawingTarget.x; 3063 3064 foreach (y; drawingTarget.y .. drawingEnd.y) { 3065 target.sliceAt(Point(drawingTarget.x, y), drawingWidth)[] = color; 3066 } 3067 } 3068 3069 /++ 3070 Draws a line 3071 +/ 3072 void drawLine(Pixmap target, Point a, Point b, Pixel color) { 3073 import std.math : sqrt; 3074 3075 // TODO: line width 3076 // TODO: anti-aliasing (looks awful without it!) 3077 3078 float deltaX = b.x - a.x; 3079 float deltaY = b.y - a.y; 3080 int steps = sqrt(deltaX * deltaX + deltaY * deltaY).castTo!int; 3081 3082 float[2] step = [ 3083 (deltaX / steps), 3084 (deltaY / steps), 3085 ]; 3086 3087 foreach (i; 0 .. steps) { 3088 // dfmt off 3089 immutable Point p = a + Point( 3090 round(step[0] * i).castTo!int, 3091 round(step[1] * i).castTo!int, 3092 ); 3093 // dfmt on 3094 3095 immutable offset = linearOffset(p, target.width); 3096 target.data[offset] = color; 3097 } 3098 3099 immutable offsetEnd = linearOffset(b, target.width); 3100 target.data[offsetEnd] = color; 3101 } 3102 3103 /++ 3104 Draws an image (a source pixmap) on a target pixmap 3105 3106 Params: 3107 target = target pixmap to draw on 3108 image = source pixmap 3109 pos = top-left destination position (on the target pixmap) 3110 +/ 3111 void drawPixmap(Pixmap target, const Pixmap image, Point pos, Blend blend = blendNormal) { 3112 alias source = image; 3113 3114 immutable tRect = OriginRectangle( 3115 Size(target.width, target.height), 3116 ); 3117 3118 immutable sRect = Rectangle(pos, source.size); 3119 3120 // out of bounds? 3121 if (!tRect.intersect(sRect)) { 3122 return; 3123 } 3124 3125 immutable drawingTarget = Point( 3126 (pos.x >= 0) ? pos.x : 0, 3127 (pos.y >= 0) ? pos.y : 0, 3128 ); 3129 3130 immutable drawingEnd = Point( 3131 (sRect.right < tRect.right) ? sRect.right : tRect.right, 3132 (sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom, 3133 ); 3134 3135 immutable drawingSource = Point(drawingTarget.x, 0) - Point(sRect.pos.x, sRect.pos.y); 3136 immutable int drawingWidth = drawingEnd.x - drawingTarget.x; 3137 3138 foreach (y; drawingTarget.y .. drawingEnd.y) { 3139 blendPixels( 3140 target.sliceAt(Point(drawingTarget.x, y), drawingWidth), 3141 source.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth), 3142 blend, 3143 ); 3144 } 3145 } 3146 3147 /++ 3148 Draws an image (a subimage from a source pixmap) on a target pixmap 3149 3150 Params: 3151 target = target pixmap to draw on 3152 image = source subpixmap 3153 pos = top-left destination position (on the target pixmap) 3154 +/ 3155 void drawPixmap(Pixmap target, const SubPixmap image, Point pos, Blend blend = blendNormal) { 3156 alias source = image; 3157 3158 debug assert(source.isValid); 3159 3160 immutable tRect = OriginRectangle( 3161 Size(target.width, target.height), 3162 ); 3163 3164 immutable sRect = Rectangle(pos, source.size); 3165 3166 // out of bounds? 3167 if (!tRect.intersect(sRect)) { 3168 return; 3169 } 3170 3171 Point sourceOffset = source.offset; 3172 Point drawingTarget; 3173 Size drawingSize = source.size; 3174 3175 if (pos.x <= 0) { 3176 sourceOffset.x -= pos.x; 3177 drawingTarget.x = 0; 3178 drawingSize.width += pos.x; 3179 } else { 3180 drawingTarget.x = pos.x; 3181 } 3182 3183 if (pos.y <= 0) { 3184 sourceOffset.y -= pos.y; 3185 drawingTarget.y = 0; 3186 drawingSize.height += pos.y; 3187 } else { 3188 drawingTarget.y = pos.y; 3189 } 3190 3191 Point drawingEnd = drawingTarget + drawingSize.castTo!Point(); 3192 if (drawingEnd.x >= target.width) { 3193 drawingSize.width -= (drawingEnd.x - target.width); 3194 } 3195 if (drawingEnd.y >= target.height) { 3196 drawingSize.height -= (drawingEnd.y - target.height); 3197 } 3198 3199 auto dst = SubPixmap(target, drawingTarget, drawingSize); 3200 auto src = const(SubPixmap)( 3201 source.source, 3202 drawingSize, 3203 sourceOffset, 3204 ); 3205 3206 src.xferTo(dst, blend); 3207 } 3208 3209 /++ 3210 Draws a sprite from a spritesheet 3211 +/ 3212 void drawSprite(Pixmap target, const SpriteSheet sheet, int spriteIndex, Point pos, Blend blend = blendNormal) { 3213 immutable tRect = OriginRectangle( 3214 Size(target.width, target.height), 3215 ); 3216 3217 immutable spriteOffset = sheet.getSpritePixelOffset2D(spriteIndex); 3218 immutable sRect = Rectangle(pos, sheet.spriteSize); 3219 3220 // out of bounds? 3221 if (!tRect.intersect(sRect)) { 3222 return; 3223 } 3224 3225 immutable drawingTarget = Point( 3226 (pos.x >= 0) ? pos.x : 0, 3227 (pos.y >= 0) ? pos.y : 0, 3228 ); 3229 3230 immutable drawingEnd = Point( 3231 (sRect.right < tRect.right) ? sRect.right : tRect.right, 3232 (sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom, 3233 ); 3234 3235 immutable drawingSource = 3236 spriteOffset 3237 + Point(drawingTarget.x, 0) 3238 - Point(sRect.pos.x, sRect.pos.y); 3239 immutable int drawingWidth = drawingEnd.x - drawingTarget.x; 3240 3241 foreach (y; drawingTarget.y .. drawingEnd.y) { 3242 blendPixels( 3243 target.sliceAt(Point(drawingTarget.x, y), drawingWidth), 3244 sheet.pixmap.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth), 3245 blend, 3246 ); 3247 } 3248 }