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 }