1 // Written in the D programming language.
2 
3 /**
4     This module implements the RGB _color type.
5 
6     Authors:    Manu Evans
7     Copyright:  Copyright (c) 2015, Manu Evans.
8     License:    $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0)
9     Source:     $(PHOBOSSRC ggplotd/color/rgb.d)
10 */
11 module ggplotd.color.rgb;
12 
13 import ggplotd.color;
14 import ggplotd.color.conv;
15 
16 import std.traits : isInstanceOf, isNumeric, isIntegral, isFloatingPoint, isSigned, isSomeChar, Unqual;
17 import std.typetuple : TypeTuple;
18 import std.typecons : tuple;
19 
20 @safe pure nothrow @nogc:
21 
22 
23 /**
24 Detect whether $(D T) is an RGB color.
25 */
26 enum isRGB(T) = isInstanceOf!(RGB, T);
27 
28 ///
29 unittest
30 {
31     static assert(isRGB!(RGB!("bgr", ushort)) == true);
32     static assert(isRGB!LA8 == true);
33     static assert(isRGB!int == false);
34 }
35 
36 
37 // DEBATE: which should it be?
38 template defaultAlpha(T)
39 {
40 /+
41     enum defaultAlpha = isFloatingPoint!T ? T(1) : T.max;
42 +/
43     enum defaultAlpha = T(0);
44 }
45 
46 
47 /**
48 Enum of RGB color spaces.
49 */
50 enum RGBColorSpace
51 {
52     /** sRGB, HDTV (ITU-R BT.709) */
53     sRGB,
54     /** sRGB with gamma 2.2 */
55     sRGB_Gamma2_2,
56 
57     // custom color space will disable automatic color spoace conversions
58     custom = -1
59 }
60 
61 
62 /**
63 An RGB color, parameterised with components, component type, and color space specification.
64 
65 Params: components_ = Components that shall be available. Struct is populated with components in the order specified.
66                       Valid components are:
67                         "r" = red
68                         "g" = green
69                         "b" = blue
70                         "a" = alpha
71                         "l" = luminance
72                         "x" = placeholder/padding (no significant value)
73         ComponentType_ = Type for the color channels. May be a basic integer or floating point type.
74         linear_ = Color is stored with linear luminance.
75         colorSpace_ = Color will be within the specified color space.
76 */
77 struct RGB(string components_, ComponentType_, bool linear_ = false, RGBColorSpace colorSpace_ = RGBColorSpace.sRGB)
78     if(isNumeric!ComponentType_)
79 {
80 @safe pure nothrow @nogc:
81 
82     // RGB colors may only contain components 'rgb', or 'l' (luminance)
83     // They may also optionally contain an 'a' (alpha) component, and 'x' (unused) components
84     static assert(allIn!("rgblax", components), "Invalid Color component '"d ~ notIn!("rgblax", components) ~ "'. RGB colors may only contain components: r, g, b, l, a, x"d);
85     static assert(anyIn!("rgbal", components), "RGB colors must contain at least one component of r, g, b, l, a.");
86     static assert(!canFind!(components, 'l') || !anyIn!("rgb", components), "RGB colors may not contain rgb AND luminance components together.");
87 
88     /** Type of the color components. */
89     alias ComponentType = ComponentType_;
90     /** The color components that were specified. */
91     enum string components = components_;
92     /** The color space specified. */
93     enum RGBColorSpace colorSpace = colorSpace_;
94     /** If the color is stored linearly (without gamma applied). */
95     enum bool linear = linear_;
96 
97 
98     // mixin will emit members for components
99     template Components(string components)
100     {
101         static if(components.length == 0)
102             enum Components = "";
103         else
104             enum Components = ComponentType.stringof ~ ' ' ~ components[0] ~ " = 0;\n" ~ Components!(components[1..$]);
105     }
106     mixin(Components!components);
107 
108     /** Test if a particular component is present. */
109     enum bool hasComponent(char c) = mixin("is(typeof(this."~c~"))");
110     /** If the color has alpha. */
111     enum bool hasAlpha = hasComponent!'a';
112 
113 
114     /** Return the RGB tristimulus values as a tuple.
115         These will always be ordered (R, G, B).
116         Any color channels not present will be 0. */
117     @property auto tristimulus() const
118     {
119         static if(hasComponent!'l')
120         {
121             return tuple(l, l, l);
122         }
123         else
124         {
125             static if(!hasComponent!'r')
126                 enum r = ComponentType(0);
127             static if(!hasComponent!'g')
128                 enum g = ComponentType(0);
129             static if(!hasComponent!'b')
130                 enum b = ComponentType(0);
131             return tuple(r, g, b);
132         }
133     }
134     ///
135     unittest
136     {
137         // BGR color
138         auto c = BGR8(255, 128, 10);
139 
140         // tristimulus always returns tuple in RGB order
141     }
142 
143     /** Return the RGB tristimulus values + alpha as a tuple.
144         These will always be ordered (R, G, B, A). */
145     @property auto tristimulusWithAlpha() const
146     {
147         static if(!hasAlpha)
148             enum a = defaultAlpha!ComponentType;
149         return tuple(tristimulus.expand, a);
150     }
151     ///
152     unittest
153     {
154         // BGRA color
155         auto c = BGRA8(255, 128, 10, 80);
156 
157         // tristimulusWithAlpha always returns tuple in RGBA order
158     }
159 
160     /** Construct a color from RGB and optional alpha values. */
161     this(ComponentType r, ComponentType g, ComponentType b, ComponentType a = defaultAlpha!ComponentType)
162     {
163         foreach(c; TypeTuple!("r","g","b","a"))
164             mixin(ComponentExpression!("this._ = _;", c, null));
165         static if(canFind!(components, 'l'))
166             this.l = toGrayscale!(linear, colorSpace)(r, g, b); // ** Contentious? I this this is most useful
167     }
168 
169     /** Construct a color from a luminance and optional alpha value. */
170     this(ComponentType l, ComponentType a = defaultAlpha!ComponentType)
171     {
172         foreach(c; TypeTuple!("l","r","g","b"))
173             mixin(ComponentExpression!("this._ = l;", c, null));
174         static if(canFind!(components, 'a'))
175             this.a = a;
176     }
177 
178     /** Construct a color from a hex string. */
179     this(C)(const(C)[] hex) if(isSomeChar!C)
180     {
181         import ggplotd.color.conv: colorFromString;
182         this = colorFromString!(typeof(this))(hex);
183     }
184 
185     // casts
186     Color opCast(Color)() const if(isColor!Color)
187     {
188         return convertColor!Color(this);
189     }
190 
191     // comparison
192     bool opEquals(typeof(this) rh) const
193     {
194         // this is required to exclude 'x' components from equality comparisons
195         return tristimulusWithAlpha == rh.tristimulusWithAlpha;
196     }
197 
198     // operators
199     mixin ColorOperators!AllComponents;
200 
201     unittest
202     {
203         alias UnsignedRGB = RGB!("rgb", ubyte);
204         alias SignedRGBX = RGB!("rgbx", byte);
205         alias FloatRGBA = RGB!("rgba", float);
206 
207         // test construction
208         static assert(UnsignedRGB("0x908000FF")  == UnsignedRGB(0x80,0,0xFF));
209         static assert(FloatRGBA("0x908000FF")    == FloatRGBA(float(0x80)/float(0xFF),0,1,float(0x90)/float(0xFF)));
210 
211         // test operators
212         static assert(-SignedRGBX(1,2,3) == SignedRGBX(-1,-2,-3));
213         static assert(-FloatRGBA(1,2,3)  == FloatRGBA(-1,-2,-3));
214 
215         static assert(UnsignedRGB(10,20,30)  + UnsignedRGB(4,5,6) == UnsignedRGB(14,25,36));
216         static assert(SignedRGBX(10,20,30)   + SignedRGBX(4,5,6)  == SignedRGBX(14,25,36));
217         static assert(FloatRGBA(10,20,30,40) + FloatRGBA(4,5,6,7) == FloatRGBA(14,25,36,47));
218 
219         static assert(UnsignedRGB(10,20,30)  - UnsignedRGB(4,5,6) == UnsignedRGB(6,15,24));
220         static assert(SignedRGBX(10,20,30)   - SignedRGBX(4,5,6)  == SignedRGBX(6,15,24));
221         static assert(FloatRGBA(10,20,30,40) - FloatRGBA(4,5,6,7) == FloatRGBA(6,15,24,33));
222 
223         static assert(UnsignedRGB(10,20,30)  * UnsignedRGB(0,1,2) == UnsignedRGB(0,20,60));
224         static assert(SignedRGBX(10,20,30)   * SignedRGBX(0,1,2)  == SignedRGBX(0,20,60));
225         static assert(FloatRGBA(10,20,30,40) * FloatRGBA(0,1,2,3) == FloatRGBA(0,20,60,120));
226 
227         static assert(UnsignedRGB(10,20,30)  / UnsignedRGB(1,2,3) == UnsignedRGB(10,10,10));
228         static assert(SignedRGBX(10,20,30)   / SignedRGBX(1,2,3)  == SignedRGBX(10,10,10));
229         static assert(FloatRGBA(2,4,8,16)    / FloatRGBA(1,2,4,8) == FloatRGBA(2,2,2,2));
230 
231         static assert(UnsignedRGB(10,20,30)  * 2 == UnsignedRGB(20,40,60));
232         static assert(SignedRGBX(10,20,30)   * 2 == SignedRGBX(20,40,60));
233         static assert(FloatRGBA(10,20,30,40) * 2 == FloatRGBA(20,40,60,80));
234 
235         static assert(UnsignedRGB(10,20,30)  / 2 == UnsignedRGB(5,10,15));
236         static assert(SignedRGBX(10,20,30)   / 2 == SignedRGBX(5,10,15));
237         static assert(FloatRGBA(10,20,30,40) / 2 == FloatRGBA(5,10,15,20));
238     }
239 
240 private:
241     alias AllComponents = TypeTuple!("l","r","g","b","a");
242     alias ParentColor = XYZ!(FloatTypeFor!ComponentType);
243 }
244 
245 
246 /** Convert a value from gamma compressed space to linear. */
247 T toLinear(RGBColorSpace src, T)(T v) if(isFloatingPoint!T)
248 {
249     enum ColorSpace = RGBColorSpaceDefs!T[src];
250     return ColorSpace.toLinear(v);
251 }
252 /** Convert a value to gamma compressed space. */
253 T toGamma(RGBColorSpace src, T)(T v) if(isFloatingPoint!T)
254 {
255     enum ColorSpace = RGBColorSpaceDefs!T[src];
256     return ColorSpace.toGamma(v);
257 }
258 
259 /** Convert a color to linear space. */
260 auto toLinear(C)(C color) if(isRGB!C)
261 {
262     return cast(RGB!(C.components, C.ComponentType, true, C.colorSpace))color;
263 }
264 /** Convert a color to gamma space. */
265 auto toGamma(C)(C color) if(isRGB!C)
266 {
267     return cast(RGB!(C.components, C.ComponentType, false, C.colorSpace))color;
268 }
269 
270 
271 package:
272 //
273 // Below exists a bunch of machinery for converting between RGB color spaces
274 //
275 
276 import ggplotd.color.xyz;
277 
278 // RGB color space definitions
279 struct RGBColorSpaceDef(F)
280 {
281     alias GammaFunc = F function(F v) pure nothrow @nogc @safe;
282 
283     string name;
284 
285     GammaFunc toGamma;
286     GammaFunc toLinear;
287 
288     xyY!F white;
289     xyY!F red;
290     xyY!F green;
291     xyY!F blue;
292 }
293 
294 enum RGBColorSpaceDefs(F) = [
295     RGBColorSpaceDef!F("sRGB",           &linearTosRGB!F,         &sRGBToLinear!F,         WhitePoint!F.D65, xyY!F(0.6400, 0.3300, 0.212656), xyY!F(0.3000, 0.6000, 0.715158), xyY!F(0.1500, 0.0600, 0.072186)),
296     RGBColorSpaceDef!F("sRGB Simple",    &linearToGamma!(F, 2.2), &gammaToLinear!(F, 2.2), WhitePoint!F.D65, xyY!F(0.6400, 0.3300, 0.212656), xyY!F(0.3000, 0.6000, 0.715158), xyY!F(0.1500, 0.0600, 0.072186)),
297 
298 //    RGBColorSpaceDef!F("Rec601",           &linearTosRGB!F,         &sRGBToLinear!F,         WhitePoint!F.D65, xyY!F(0.6400, 0.3300, 0.299),    xyY!F(0.3000, 0.6000, 0.587),    xyY!F(0.1500, 0.0600, 0.114)),
299 //    RGBColorSpaceDef!F("Rec709",           &linearTosRGB!F,         &sRGBToLinear!F,         WhitePoint!F.D65, xyY!F(0.6400, 0.3300, 0.212656), xyY!F(0.3000, 0.6000, 0.715158), xyY!F(0.1500, 0.0600, 0.072186)),
300 //    RGBColorSpaceDef!F("Rec2020",          &linearToRec2020!F,      &Rec2020ToLinear!F,      WhitePoint!F.D65, xyY!F(0.708,  0.292,  0.2627),   xyY!F(0.170,  0.797,  0.6780),   xyY!F(0.131,  0.046,  0.0593)),
301 ];
302 
303 template RGBColorSpaceMatrix(RGBColorSpace cs, F)
304 {
305     enum F[3] ToXYZ(xyY!F c) = [ c.x/c.y, F(1), (F(1)-c.x-c.y)/c.y ];
306 
307     // get the color space definition
308     enum def = RGBColorSpaceDefs!F[cs];
309     // build a matrix from the 3 color vectors
310     enum r = def.red, g = def.green, b = def.blue;
311     enum m = transpose([ ToXYZ!r, ToXYZ!g, ToXYZ!b ]);
312 
313     // multiply by the whitepoint
314     enum w = [ (cast(XYZ!F)(def.white)).tupleof ];
315     enum s = multiply(inverse(m), w);
316 
317     // return colorspace matrix (RGB -> XYZ)
318     enum F[3][3] RGBColorSpaceMatrix = [[ m[0][0]*s[0], m[0][1]*s[1], m[0][2]*s[2] ],
319                                         [ m[1][0]*s[0], m[1][1]*s[1], m[1][2]*s[2] ],
320                                         [ m[2][0]*s[0], m[2][1]*s[1], m[2][2]*s[2] ]];
321 }
322 
323 
324 T linearTosRGB(T)(T s) if(isFloatingPoint!T)
325 {
326     if(s <= T(0.0031308))
327         return T(12.92) * s;
328     else
329         return T(1.055) * s^^T(1.0/2.4) - T(0.055);
330 }
331 T sRGBToLinear(T)(T s) if(isFloatingPoint!T)
332 {
333     if(s <= T(0.04045))
334         return s / T(12.92);
335     else
336         return ((s + T(0.055)) / T(1.055))^^T(2.4);
337 }
338 
339 T linearToGamma(T, T gamma)(T v) if(isFloatingPoint!T)
340 {
341     return v^^T(1.0/gamma);
342 }
343 T gammaToLinear(T, T gamma)(T v) if(isFloatingPoint!T)
344 {
345     return v^^T(gamma);
346 }
347 
348 T toGrayscale(bool linear, RGBColorSpace colorSpace = RGBColorSpace.sRGB, T)(T r, T g, T b) pure if(isFloatingPoint!T)
349 {
350     // calculate the luminance (Y) value by multiplying the Y row of the XYZ matrix with the color
351     enum YAxis = RGBColorSpaceMatrix!(colorSpace, T)[1];
352 
353     static if(linear)
354     {
355         return YAxis[0]*r + YAxis[1]*g + YAxis[2]*b;
356     }
357     else
358     {
359         // sRGB Luma' coefficients match the Y axis. Assume other RGB color spaces also follow the same pattern(?)
360         // Rec.709 (HDTV) was also refined to suit, so it will work without special-case
361         // TODO: When we support Rec.601 (SDTV), we need to special-case for Luma' coefficients: 0.299, 0.587, 0.114
362 
363         return YAxis[0]*r + YAxis[1]*g + YAxis[2]*b;
364     }
365 }
366 T toGrayscale(bool linear, RGBColorSpace colorSpace = RGBColorSpace.sRGB, T)(T r, T g, T b) pure if(isIntegral!T)
367 {
368     import ggplotd.color.conv: convertPixelType;
369     alias F = FloatTypeFor!T;
370     return convertPixelType!T(toGrayscale!(linear, colorSpace)(convertPixelType!F(r), convertPixelType!F(g), convertPixelType!F(b)));
371 }
372 
373 
374 // helpers to parse color components from color component string
375 template canFind(string s, char c)
376 {
377     static if(s.length == 0)
378         enum canFind = false;
379     else
380         enum canFind = s[0] == c || canFind!(s[1..$], c);
381 }
382 template allIn(string s, string chars)
383 {
384     static if(chars.length == 0)
385         enum allIn = true;
386     else
387         enum allIn = canFind!(s, chars[0]) && allIn!(s, chars[1..$]);
388 }
389 template anyIn(string s, string chars)
390 {
391     static if(chars.length == 0)
392         enum anyIn = false;
393     else
394         enum anyIn = canFind!(s, chars[0]) || anyIn!(s, chars[1..$]);
395 }
396 template notIn(string s, string chars)
397 {
398     static if(chars.length == 0)
399         enum notIn = char(0);
400     else static if(!canFind!(s, chars[0]))
401         enum notIn = chars[0];
402     else
403         enum notIn = notIn!(s, chars[1..$]);
404 }
405 
406 unittest
407 {
408     static assert(canFind!("string", 'i'));
409     static assert(!canFind!("string", 'x'));
410     static assert(allIn!("string", "sgi"));
411     static assert(!allIn!("string", "sgix"));
412     static assert(anyIn!("string", "sx"));
413     static assert(!anyIn!("string", "x"));
414 }
415 
416 
417 // 3d linear algebra functions (this would ideally live somewhere else...)
418 F[3] multiply(F)(F[3][3] m1, F[3] v)
419 {
420     return [ m1[0][0]*v[0] + m1[0][1]*v[1] + m1[0][2]*v[2],
421              m1[1][0]*v[0] + m1[1][1]*v[1] + m1[1][2]*v[2],
422              m1[2][0]*v[0] + m1[2][1]*v[1] + m1[2][2]*v[2] ];
423 }
424 
425 F[3][3] multiply(F)(F[3][3] m1, F[3][3] m2)
426 {
427     return [[ m1[0][0]*m2[0][0] + m1[0][1]*m2[1][0] + m1[0][2]*m2[2][0],
428               m1[0][0]*m2[0][1] + m1[0][1]*m2[1][1] + m1[0][2]*m2[2][1],
429               m1[0][0]*m2[0][2] + m1[0][1]*m2[1][2] + m1[0][2]*m2[2][2] ],
430             [ m1[1][0]*m2[0][0] + m1[1][1]*m2[1][0] + m1[1][2]*m2[2][0],
431               m1[1][0]*m2[0][1] + m1[1][1]*m2[1][1] + m1[1][2]*m2[2][1],
432               m1[1][0]*m2[0][2] + m1[1][1]*m2[1][2] + m1[1][2]*m2[2][2] ],
433             [ m1[2][0]*m2[0][0] + m1[2][1]*m2[1][0] + m1[2][2]*m2[2][0],
434               m1[2][0]*m2[0][1] + m1[2][1]*m2[1][1] + m1[2][2]*m2[2][1],
435               m1[2][0]*m2[0][2] + m1[2][1]*m2[1][2] + m1[2][2]*m2[2][2] ]];
436 }
437 
438 F[3][3] transpose(F)(F[3][3] m)
439 {
440     return [[ m[0][0], m[1][0], m[2][0] ],
441             [ m[0][1], m[1][1], m[2][1] ],
442             [ m[0][2], m[1][2], m[2][2] ]];
443 }
444 
445 F determinant(F)(F[3][3] m)
446 {
447     return m[0][0] * (m[1][1]*m[2][2] - m[2][1]*m[1][2]) -
448            m[0][1] * (m[1][0]*m[2][2] - m[1][2]*m[2][0]) +
449            m[0][2] * (m[1][0]*m[2][1] - m[1][1]*m[2][0]);
450 }
451 
452 F[3][3] inverse(F)(F[3][3] m)
453 {
454     F det = determinant(m);
455     assert(det != 0, "Matrix is not invertible!");
456 
457     F invDet = F(1)/det;
458     return [[ (m[1][1]*m[2][2] - m[2][1]*m[1][2]) * invDet,
459               (m[0][2]*m[2][1] - m[0][1]*m[2][2]) * invDet,
460               (m[0][1]*m[1][2] - m[0][2]*m[1][1]) * invDet ],
461             [ (m[1][2]*m[2][0] - m[1][0]*m[2][2]) * invDet,
462               (m[0][0]*m[2][2] - m[0][2]*m[2][0]) * invDet,
463               (m[1][0]*m[0][2] - m[0][0]*m[1][2]) * invDet ],
464             [ (m[1][0]*m[2][1] - m[2][0]*m[1][1]) * invDet,
465               (m[2][0]*m[0][1] - m[0][0]*m[2][1]) * invDet,
466               (m[0][0]*m[1][1] - m[1][0]*m[0][1]) * invDet ]];
467 }