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 }