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