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