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