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 }