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