1 /** 2 * Image maps. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.graphics.view; 15 16 import std.functional; 17 import std.typetuple; 18 19 /// A view is any type which provides a width, height, 20 /// and can be indexed to get the color at a specific 21 /// coordinate. 22 enum isView(T) = 23 is(typeof(T.init.w) : size_t) && // width 24 is(typeof(T.init.h) : size_t) && // height 25 is(typeof(T.init[0, 0]) ); // color information 26 27 /// Returns the color type of the specified view. 28 /// By convention, colors are structs with numeric 29 /// fields named after the channel they indicate. 30 alias ViewColor(T) = typeof(T.init[0, 0]); 31 32 /// Views can be read-only or writable. 33 enum isWritableView(T) = 34 isView!T && 35 is(typeof(T.init[0, 0] = ViewColor!T.init)); 36 37 /// Optionally, a view can also provide direct pixel 38 /// access. We call these "direct views". 39 enum isDirectView(T) = 40 isView!T && 41 is(typeof(T.init.scanline(0)) : ViewColor!T[]); 42 43 /// Mixin which implements view primitives on top of 44 /// existing direct view primitives. 45 mixin template DirectView() 46 { 47 import std.traits : Unqual; 48 alias COLOR = Unqual!(typeof(scanline(0)[0])); 49 50 /// Implements the view[x, y] operator. 51 ref inout(COLOR) opIndex(int x, int y) inout 52 { 53 return scanline(y)[x]; 54 } 55 56 /// Allows array-like view[y][x] access. 57 auto opIndex(int y) 58 { 59 return scanline(y); 60 } 61 62 /// Implements the view[x, y] = c operator. 63 COLOR opIndexAssign(COLOR value, int x, int y) 64 { 65 return scanline(y)[x] = value; 66 } 67 } 68 69 // *************************************************************************** 70 71 /// Returns a view which calculates pixels 72 /// on-demand using the specified formula. 73 template procedural(alias formula) 74 { 75 alias fun = binaryFun!(formula, "x", "y"); 76 alias COLOR = typeof(fun(0, 0)); 77 78 auto procedural(int w, int h) 79 { 80 struct Procedural 81 { 82 int w, h; 83 84 auto ref COLOR opIndex(int x, int y) 85 { 86 assert(x >= 0 && y >= 0 && x < w && y < h); 87 return fun(x, y); 88 } 89 } 90 return Procedural(w, h); 91 } 92 } 93 94 /// Returns a view of the specified dimensions 95 /// and same solid color. 96 auto solid(COLOR)(COLOR c, int w, int h) 97 { 98 return procedural!((x, y) => c)(w, h); 99 } 100 101 /// Return a 1x1 view of the specified color. 102 /// Useful for testing. 103 auto onePixel(COLOR)(COLOR c) 104 { 105 return solid(c, 1, 1); 106 } 107 108 unittest 109 { 110 assert(onePixel(42)[0, 0] == 42); 111 } 112 113 // *************************************************************************** 114 115 /// Blits a view onto another. 116 /// The views must have the same size. 117 void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst) 118 if (isView!SRC && isWritableView!DST) 119 { 120 assert(src.w == dst.w && src.h == dst.h, "View size mismatch"); 121 foreach (y; 0..src.h) 122 { 123 static if (isDirectView!SRC && isDirectView!DST) 124 dst.scanline(y)[] = src.scanline(y)[]; 125 else 126 { 127 foreach (x; 0..src.w) 128 dst[x, y] = src[x, y]; 129 } 130 } 131 } 132 133 /// Helper function to blit an image onto another at a specified location. 134 void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst, int x, int y) 135 { 136 src.blitTo(dst.crop(x, y, x+src.w, y+src.h)); 137 } 138 139 void safeBlitTo(SRC, DST)(auto ref SRC src, auto ref DST dst, int x, int y) 140 { 141 // TODO: refactor into safeCrop 142 int sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1; 143 sx1 = src.w; 144 sy1 = src.h; 145 dx0 = x; 146 dy0 = y; 147 dx1 = x + src.w; 148 dy1 = y + src.h; 149 if (dx0 < 0) { auto v = -dx0; sx0 += v; dx0 += v; } 150 if (dy0 < 0) { auto v = -dy0; sy0 += v; dy0 += v; } 151 if (dx1 > dst.w) { auto v = dx1 - dst.w; sx1 -= v; dx1 -= v; } 152 if (dy1 > dst.h) { auto v = dy1 - dst.h; sy1 -= v; dy1 -= v; } 153 if (dx0 > dx1) { dx1 = dx0; sx1 = sx0; } 154 if (dy0 > dy1) { dy1 = dy0; sy1 = sy0; } 155 assert(sx1 - sx0 == dx1 - dx0); 156 assert(sy1 - sy0 == dy1 - dy0); 157 blitTo( 158 src.crop(sx0, sy0, sx1, sy1), 159 dst.crop(dx0, dy0, dx1, dy1), 160 ); 161 } 162 163 /// Default implementation for the .size method. 164 /// Asserts that the view has the desired size. 165 void size(V)(auto ref V src, int w, int h) 166 if (isView!V) 167 { 168 import std.string : format; 169 assert(src.w == w && src.h == h, 170 "Wrong size for %s: need (%s,%s), have (%s,%s)" 171 .format(V.stringof, w, h, src.w, src.h)); 172 } 173 174 // *************************************************************************** 175 176 /// Mixin which implements view primitives on top of 177 /// another view, using a coordinate transform function. 178 mixin template Warp(V) 179 if (isView!V) 180 { 181 V src; 182 183 auto ref ViewColor!V opIndex(int x, int y) 184 { 185 warp(x, y); 186 return src[x, y]; 187 } 188 189 static if (isWritableView!V) 190 ViewColor!V opIndexAssign(ViewColor!V value, int x, int y) 191 { 192 warp(x, y); 193 return src[x, y] = value; 194 } 195 } 196 197 /// Crop a view to the specified rectangle. 198 auto crop(V)(auto ref V src, int x0, int y0, int x1, int y1) 199 if (isView!V) 200 { 201 assert( 0 <= x0 && 0 <= y0); 202 assert(x0 <= x1 && y0 <= y1); 203 assert(x1 <= src.w && y1 <= src.h); 204 205 static struct Crop 206 { 207 mixin Warp!V; 208 209 int x0, y0, x1, y1; 210 211 @property int w() { return x1-x0; } 212 @property int h() { return y1-y0; } 213 214 void warp(ref int x, ref int y) 215 { 216 x += x0; 217 y += y0; 218 } 219 220 static if (isDirectView!V) 221 ViewColor!V[] scanline(int y) 222 { 223 return src.scanline(y0+y)[x0..x1]; 224 } 225 } 226 227 static assert(isDirectView!V == isDirectView!Crop); 228 229 return Crop(src, x0, y0, x1, y1); 230 } 231 232 unittest 233 { 234 auto g = procedural!((x, y) => y)(1, 256); 235 auto c = g.crop(0, 10, 1, 20); 236 assert(c[0, 0] == 10); 237 } 238 239 /// Tile another view. 240 auto tile(V)(auto ref V src, int w, int h) 241 if (isView!V) 242 { 243 static struct Tile 244 { 245 mixin Warp!V; 246 247 int w, h; 248 249 void warp(ref int x, ref int y) 250 { 251 assert(x >= 0 && y >= 0 && x < w && y < h); 252 x = x % src.w; 253 y = y % src.h; 254 } 255 } 256 257 return Tile(src, w, h); 258 } 259 260 unittest 261 { 262 auto i = onePixel(4); 263 auto t = i.tile(100, 100); 264 assert(t[12, 34] == 4); 265 } 266 267 /// Present a resized view using nearest-neighbor interpolation. 268 auto nearestNeighbor(V)(auto ref V src, int w, int h) 269 if (isView!V) 270 { 271 static struct NearestNeighbor 272 { 273 mixin Warp!V; 274 275 int w, h; 276 277 void warp(ref int x, ref int y) 278 { 279 x = cast(int)(cast(long)x * src.w / w); 280 y = cast(int)(cast(long)y * src.h / h); 281 } 282 } 283 284 return NearestNeighbor(src, w, h); 285 } 286 287 unittest 288 { 289 auto g = procedural!((x, y) => x+10*y)(10, 10); 290 auto n = g.nearestNeighbor(100, 100); 291 assert(n[12, 34] == 31); 292 } 293 294 /// Swap the X and Y axes (flip the image diagonally). 295 auto flipXY(V)(auto ref V src) 296 { 297 static struct FlipXY 298 { 299 mixin Warp!V; 300 301 @property int w() { return src.h; } 302 @property int h() { return src.w; } 303 304 void warp(ref int x, ref int y) 305 { 306 import std.algorithm; 307 swap(x, y); 308 } 309 } 310 311 return FlipXY(src); 312 } 313 314 // *************************************************************************** 315 316 /// Return a view of src with the coordinates transformed 317 /// according to the given formulas 318 template warp(string xExpr, string yExpr) 319 { 320 auto warp(V)(auto ref V src) 321 if (isView!V) 322 { 323 static struct Warped 324 { 325 mixin Warp!V; 326 327 @property int w() { return src.w; } 328 @property int h() { return src.h; } 329 330 void warp(ref int x, ref int y) 331 { 332 auto nx = mixin(xExpr); 333 auto ny = mixin(yExpr); 334 x = nx; y = ny; 335 } 336 337 private void testWarpY()() 338 { 339 int y; 340 y = mixin(yExpr); 341 } 342 343 /// If the x coordinate is not affected and y does not 344 /// depend on x, we can transform entire scanlines. 345 static if (xExpr == "x" && 346 __traits(compiles, testWarpY()) && 347 isDirectView!V) 348 ViewColor!V[] scanline(int y) 349 { 350 return src.scanline(mixin(yExpr)); 351 } 352 } 353 354 return Warped(src); 355 } 356 } 357 358 /// ditto 359 template warp(alias pred) 360 { 361 auto warp(V)(auto ref V src) 362 if (isView!V) 363 { 364 struct Warped 365 { 366 mixin Warp!V; 367 368 @property int w() { return src.w; } 369 @property int h() { return src.h; } 370 371 alias warp = binaryFun!(pred, "x", "y"); 372 } 373 374 return Warped(src); 375 } 376 } 377 378 /// Return a view of src with the x coordinate inverted. 379 alias hflip = warp!(q{w-x-1}, q{y}); 380 381 /// Return a view of src with the y coordinate inverted. 382 alias vflip = warp!(q{x}, q{h-y-1}); 383 384 /// Return a view of src with both coordinates inverted. 385 alias flip = warp!(q{w-x-1}, q{h-y-1}); 386 387 unittest 388 { 389 import ae.utils.graphics.image; 390 auto vband = procedural!((x, y) => y)(1, 256).copy(); 391 auto flipped = vband.vflip(); 392 assert(flipped[0, 1] == 254); 393 static assert(isDirectView!(typeof(flipped))); 394 395 import std.algorithm; 396 auto w = vband.warp!((ref x, ref y) { swap(x, y); }); 397 } 398 399 /// Rotate a view 90 degrees clockwise. 400 auto rotateCW(V)(auto ref V src) 401 { 402 return src.flipXY().hflip(); 403 } 404 405 /// Rotate a view 90 degrees counter-clockwise. 406 auto rotateCCW(V)(auto ref V src) 407 { 408 return src.flipXY().vflip(); 409 } 410 411 unittest 412 { 413 auto g = procedural!((x, y) => x+10*y)(10, 10); 414 int[] corners(V)(V v) { return [v[0, 0], v[9, 0], v[0, 9], v[9, 9]]; } 415 assert(corners(g ) == [ 0, 9, 90, 99]); 416 assert(corners(g.flipXY ) == [ 0, 90, 9, 99]); 417 assert(corners(g.rotateCW ) == [90, 0, 99, 9]); 418 assert(corners(g.rotateCCW) == [ 9, 99, 0, 90]); 419 } 420 421 // *************************************************************************** 422 423 /// Return a view with the given views concatenated vertically. 424 /// Assumes all views have the same width. 425 /// Creates an index for fast row -> source view lookup. 426 auto vjoiner(V)(V[] views) 427 if (isView!V) 428 { 429 static struct VJoiner 430 { 431 struct Child { V view; int y; } 432 Child[] children; 433 size_t[] index; 434 435 @property int w() { return children[0].view.w; } 436 int h; 437 438 this(V[] views) 439 { 440 children = new Child[views.length]; 441 int y = 0; 442 foreach (i, ref v; views) 443 { 444 assert(v.w == views[0].w, "Inconsistent width"); 445 children[i] = Child(v, y); 446 y += v.h; 447 } 448 449 h = y; 450 451 index = new size_t[h]; 452 453 foreach (i, ref child; children) 454 index[child.y .. child.y + child.view.h] = i; 455 } 456 457 auto ref ViewColor!V opIndex(int x, int y) 458 { 459 auto child = &children[index[y]]; 460 return child.view[x, y - child.y]; 461 } 462 463 static if (isWritableView!V) 464 ViewColor!V opIndexAssign(ViewColor!V value, int x, int y) 465 { 466 auto child = &children[index[y]]; 467 return child.view[x, y - child.y] = value; 468 } 469 470 static if (isDirectView!V) 471 ViewColor!V[] scanline(int y) 472 { 473 auto child = &children[index[y]]; 474 return child.view.scanline(y - child.y); 475 } 476 } 477 478 return VJoiner(views); 479 } 480 481 unittest 482 { 483 import std.algorithm : map; 484 import std.array : array; 485 import std.range : iota; 486 487 auto v = 10.iota.map!onePixel.array.vjoiner(); 488 foreach (i; 0..10) 489 assert(v[0, i] == i); 490 } 491 492 // *************************************************************************** 493 494 /// Overlay the view fg over bg at a certain coordinate. 495 /// The resulting view inherits bg's size. 496 auto overlay(BG, FG)(auto ref BG bg, auto ref FG fg, int x, int y) 497 if (isView!BG && isView!FG && is(ViewColor!BG == ViewColor!FG)) 498 { 499 alias COLOR = ViewColor!BG; 500 501 static struct Overlay 502 { 503 BG bg; 504 FG fg; 505 506 int ox, oy; 507 508 @property int w() { return bg.w; } 509 @property int h() { return bg.h; } 510 511 auto ref COLOR opIndex(int x, int y) 512 { 513 if (x >= ox && y >= oy && x < ox + fg.w && y < oy + fg.h) 514 return fg[x - ox, y - oy]; 515 else 516 return bg[x, y]; 517 } 518 519 static if (isWritableView!BG && isWritableView!FG) 520 COLOR opIndexAssign(COLOR value, int x, int y) 521 { 522 if (x >= ox && y >= oy && x < ox + fg.w && y < oy + fg.h) 523 return fg[x - ox, y - oy] = value; 524 else 525 return bg[x, y] = value; 526 } 527 } 528 529 return Overlay(bg, fg, x, y); 530 } 531 532 /// Add a solid-color border around an image. 533 /// The parameters indicate the border's thickness around each side 534 /// (left, top, right, bottom in order). 535 auto border(V, COLOR)(auto ref V src, int x0, int y0, int x1, int y1, COLOR color) 536 if (isView!V && is(COLOR == ViewColor!V)) 537 { 538 return color 539 .solid( 540 x0 + src.w + x1, 541 y0 + src.h + y1, 542 ) 543 .overlay(src, x0, y0); 544 } 545 546 unittest 547 { 548 auto g = procedural!((x, y) => x+10*y)(10, 10); 549 auto b = g.border(5, 5, 5, 5, 42); 550 assert(b.w == 20); 551 assert(b.h == 20); 552 assert(b[1, 2] == 42); 553 assert(b[5, 5] == 0); 554 assert(b[14, 14] == 99); 555 assert(b[14, 15] == 42); 556 } 557 558 // *************************************************************************** 559 560 /// Alpha-blend a number of views. 561 /// The order is bottom-to-top. 562 auto blend(SRCS...)(SRCS sources) 563 if (allSatisfy!(isView, SRCS) 564 && sources.length > 0) 565 { 566 alias COLOR = ViewColor!(SRCS[0]); 567 568 foreach (src; sources) 569 assert(src.w == sources[0].w && src.h == sources[0].h, 570 "Mismatching layer size"); 571 572 static struct Blend 573 { 574 SRCS sources; 575 576 @property int w() { return sources[0].w; } 577 @property int h() { return sources[0].h; } 578 579 COLOR opIndex(int x, int y) 580 { 581 COLOR c = sources[0][x, y]; 582 foreach (ref src; sources[1..$]) 583 c = COLOR.blend(c, src[x, y]); 584 return c; 585 } 586 } 587 588 return Blend(sources); 589 } 590 591 unittest 592 { 593 import ae.utils.graphics.color : LA; 594 auto v0 = onePixel(LA( 0, 255)); 595 auto v1 = onePixel(LA(255, 100)); 596 auto vb = blend(v0, v1); 597 assert(vb[0, 0] == LA(100, 255)); 598 } 599 600 // *************************************************************************** 601 602 /// Similar to Warp, but allows warped coordinates to go out of bounds. 603 mixin template SafeWarp(V) 604 { 605 V src; 606 ViewColor!V defaultColor; 607 608 auto ref ViewColor!V opIndex(int x, int y) 609 { 610 warp(x, y); 611 if (x >= 0 && y >= 0 && x < w && y < h) 612 return src[x, y]; 613 else 614 return defaultColor; 615 } 616 617 static if (isWritableView!V) 618 ViewColor!V opIndexAssign(ViewColor!V value, int x, int y) 619 { 620 warp(x, y); 621 if (x >= 0 && y >= 0 && x < w && y < h) 622 return src[x, y] = value; 623 else 624 return defaultColor; 625 } 626 } 627 628 /// Rotate a view at an arbitrary angle (specified in radians), 629 /// around the specified point. Rotated points that fall outside of 630 /// the specified view resolve to defaultColor. 631 auto rotate(V, COLOR)(auto ref V src, double angle, COLOR defaultColor, 632 double ox, double oy) 633 if (isView!V && is(COLOR : ViewColor!V)) 634 { 635 static struct Rotate 636 { 637 mixin SafeWarp!V; 638 double theta, ox, oy; 639 640 @property int w() { return src.w; } 641 @property int h() { return src.h; } 642 643 void warp(ref int x, ref int y) 644 { 645 import std.math; 646 auto vx = x - ox; 647 auto vy = y - oy; 648 x = cast(int)round(ox + cos(theta) * vx - sin(theta) * vy); 649 y = cast(int)round(oy + sin(theta) * vx + cos(theta) * vy); 650 } 651 } 652 653 return Rotate(src, defaultColor, angle, ox, oy); 654 } 655 656 /// Rotate a view at an arbitrary angle (specified in radians) around 657 /// its center. 658 auto rotate(V, COLOR)(auto ref V src, double angle, 659 COLOR defaultColor = ViewColor!V.init) 660 if (isView!V && is(COLOR : ViewColor!V)) 661 { 662 return src.rotate(angle, defaultColor, src.w / 2.0 - 0.5, src.h / 2.0 - 0.5); 663 } 664 665 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 666 version(unittest) static import ae.utils.geometry; 667 668 unittest 669 { 670 import ae.utils.graphics.image; 671 import ae.utils.geometry; 672 auto i = Image!int(3, 3); 673 i[1, 0] = 1; 674 auto r = i.rotate(cast(double)TAU/4, 0); 675 assert(r[1, 0] == 0); 676 assert(r[0, 1] == 1); 677 } 678 679 // *************************************************************************** 680 681 /// Return a view which applies a predicate over the 682 /// underlying view's pixel colors. 683 template colorMap(alias fun) 684 { 685 auto colorMap(V)(auto ref V src) 686 if (isView!V) 687 { 688 alias OLDCOLOR = ViewColor!V; 689 alias NEWCOLOR = typeof(fun(OLDCOLOR.init)); 690 691 struct Map 692 { 693 V src; 694 695 @property int w() { return src.w; } 696 @property int h() { return src.h; } 697 698 /*auto ref*/ NEWCOLOR opIndex(int x, int y) 699 { 700 return fun(src[x, y]); 701 } 702 } 703 704 return Map(src); 705 } 706 } 707 708 /// Two-way colorMap which allows writing to the returned view. 709 template colorMap(alias getFun, alias setFun) 710 { 711 auto colorMap(V)(auto ref V src) 712 if (isView!V) 713 { 714 alias OLDCOLOR = ViewColor!V; 715 alias NEWCOLOR = typeof(getFun(OLDCOLOR.init)); 716 717 struct Map 718 { 719 V src; 720 721 @property int w() { return src.w; } 722 @property int h() { return src.h; } 723 724 NEWCOLOR opIndex(int x, int y) 725 { 726 return getFun(src[x, y]); 727 } 728 729 static if (isWritableView!V) 730 NEWCOLOR opIndexAssign(NEWCOLOR c, int x, int y) 731 { 732 return src[x, y] = setFun(c); 733 } 734 } 735 736 return Map(src); 737 } 738 } 739 740 /// Returns a view which inverts all channels. 741 // TODO: skip alpha and padding 742 alias invert = colorMap!(c => ~c, c => ~c); 743 744 unittest 745 { 746 import ae.utils.graphics.color; 747 import ae.utils.graphics.image; 748 749 auto i = onePixel(L8(1)); 750 assert(i.invert[0, 0].l == 254); 751 } 752 753 // *************************************************************************** 754 755 /// Returns the smallest window containing all 756 /// pixels that satisfy the given predicate. 757 template trim(alias fun) 758 { 759 auto trim(V)(auto ref V src) 760 { 761 int x0 = 0, y0 = 0, x1 = src.w, y1 = src.h; 762 topLoop: 763 while (y0 < y1) 764 { 765 foreach (x; 0..src.w) 766 if (fun(src[x, y0])) 767 break topLoop; 768 y0++; 769 } 770 bottomLoop: 771 while (y1 > y0) 772 { 773 foreach (x; 0..src.w) 774 if (fun(src[x, y1-1])) 775 break bottomLoop; 776 y1--; 777 } 778 779 leftLoop: 780 while (x0 < x1) 781 { 782 foreach (y; y0..y1) 783 if (fun(src[x0, y])) 784 break leftLoop; 785 x0++; 786 } 787 rightLoop: 788 while (x1 > x0) 789 { 790 foreach (y; y0..y1) 791 if (fun(src[x1-1, y])) 792 break rightLoop; 793 x1--; 794 } 795 796 return src.crop(x0, y0, x1, y1); 797 } 798 } 799 800 alias trimAlpha = trim!(c => c.a); 801 802 // *************************************************************************** 803 804 /// Splits a view into segments and 805 /// calls fun on each segment in parallel. 806 /// Returns an array of segments which 807 /// can be joined using vjoin or vjoiner. 808 template parallel(alias fun) 809 { 810 auto parallel(V)(auto ref V src, size_t chunkSize = 0) 811 if (isView!V) 812 { 813 import std.parallelism : taskPool, parallel; 814 815 auto processSegment(R)(R rows) 816 { 817 auto y0 = rows[0]; 818 auto y1 = y0 + cast(typeof(y0))rows.length; 819 auto segment = src.crop(0, y0, src.w, y1); 820 return fun(segment); 821 } 822 823 import std.range : iota, chunks; 824 if (!chunkSize) 825 chunkSize = taskPool.defaultWorkUnitSize(src.h); 826 827 auto range = src.h.iota.chunks(chunkSize); 828 alias Result = typeof(processSegment(range.front)); 829 auto result = new Result[range.length]; 830 foreach (n; range.length.iota.parallel(1)) 831 result[n] = processSegment(range[n]); 832 return result; 833 } 834 } 835 836 unittest 837 { 838 import ae.utils.graphics.image; 839 auto g = procedural!((x, y) => x+10*y)(10, 10); 840 auto i = g.parallel!(s => s.invert.copy).vjoiner; 841 assert(i[0, 0] == ~0); 842 assert(i[9, 9] == ~99); 843 }