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