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