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