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