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