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