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