1 // Written in the D programming language. 2 3 /** 4 This module implements the $(LINK2 https://en.wikipedia.org/wiki/RGB_color_space, RGB) _color type. 5 6 RGB is the most common expression of colors used in computing, where a _color is specified as some 7 amount of red, green and blue primaries. 8 9 RGB is highly parametric, and comes in many shapes and sizes, with the most common being 10 $(LINK2 https://en.wikipedia.org/wiki/SRGB, sRGB), which is conventionally used on 11 computer monitors, and standard for use on the web. 12 13 RGB colors require the RGB _color space parameters to be defined to be considered 'absolute' colors. 14 15 Authors: Manu Evans 16 Copyright: Copyright (c) 2015, Manu Evans. 17 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0) 18 Source: $(PHOBOSSRC std/experimental/color/_rgb.d) 19 */ 20 module std.experimental.color.rgb; 21 22 import std.experimental.color; 23 import std.experimental.color.colorspace; 24 import std.experimental.color.xyz : XYZ, isXYZ; 25 import std.experimental.normint; 26 27 import std.traits : isInstanceOf, isNumeric, isIntegral, isFloatingPoint, isSomeChar, Unqual; 28 import std.typetuple : TypeTuple; 29 import std.typecons : tuple; 30 31 @safe pure nothrow @nogc: 32 33 34 /** 35 Detect whether $(D_INLINECODE T) is an RGB color. 36 */ 37 enum isRGB(T) = isInstanceOf!(RGB, T); 38 39 /// 40 unittest 41 { 42 static assert(isRGB!(RGB!("bgr", ushort)) == true); 43 static assert(isRGB!LA8 == true); 44 static assert(isRGB!int == false); 45 } 46 47 48 // DEBATE: which should it be? 49 template defaultAlpha(T) 50 { 51 /+ 52 enum defaultAlpha = isFloatingPoint!T ? T(1) : T.max; 53 +/ 54 enum defaultAlpha = T(0); 55 } 56 57 58 /** 59 An RGB color, parameterised with components, component type, and color space specification. 60 61 Params: components_ = Components that shall be available. Struct is populated with components in the order specified.$(BR) 62 Valid components are:$(BR) 63 "r" = red$(BR) 64 "g" = green$(BR) 65 "b" = blue$(BR) 66 "a" = alpha$(BR) 67 "l" = luminance$(BR) 68 "x" = placeholder/padding (no significant value) 69 ComponentType_ = Type for the color channels. May be a basic integer or floating point type. 70 linear_ = Color is stored with linear luminance. 71 colorSpace_ = Color will be within the specified color space. 72 */ 73 struct RGB(string components_, ComponentType_, bool linear_ = false, RGBColorSpace colorSpace_ = RGBColorSpace.sRGB) 74 if (isNumeric!ComponentType_) 75 { 76 @safe pure: 77 78 /** Construct a color from a string. */ 79 this(C)(const(C)[] str) if (isSomeChar!C) 80 { 81 this = colorFromString!(typeof(this))(str); 82 } 83 /// 84 unittest 85 { 86 static assert(RGB8("#8000FF") == RGB8(0x80,0x00,0xFF)); 87 static assert(RGBA8("#908000FF") == RGBA8(0x80,0x00,0xFF,0x90)); 88 } 89 90 nothrow @nogc: 91 92 // RGB colors may only contain components 'rgb', or 'l' (luminance) 93 // They may also optionally contain an 'a' (alpha) component, and 'x' (unused) components 94 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); 95 static assert(anyIn!("rgbal", components), "RGB colors must contain at least one component of r, g, b, l, a."); 96 static assert(!canFind!(components, 'l') || !anyIn!("rgb", components), "RGB colors may not contain rgb AND luminance components together."); 97 98 static if (isFloatingPoint!ComponentType_) 99 { 100 /** Type of the color components. */ 101 alias ComponentType = ComponentType_; 102 } 103 else 104 { 105 /** Type of the color components. */ 106 alias ComponentType = NormalizedInt!ComponentType_; 107 } 108 109 /** The color components that were specified. */ 110 enum string components = components_; 111 /** The colors color space. */ 112 enum RGBColorSpace colorSpace = colorSpace_; 113 /** The color space descriptor. */ 114 enum RGBColorSpaceDesc!F colorSpaceDesc(F = double) = rgbColorSpaceDef!F(colorSpace_); 115 /** If the color is stored linearly (without gamma applied). */ 116 enum bool linear = linear_; 117 118 119 // mixin will emit members for components 120 template Components(string components) 121 { 122 static if (components.length == 0) 123 enum Components = ""; 124 else 125 enum Components = ComponentType.stringof ~ ' ' ~ components[0] ~ " = 0;\n" ~ Components!(components[1..$]); 126 } 127 mixin(Components!components); 128 129 /** Test if a particular component is present. */ 130 enum bool hasComponent(char c) = mixin("is(typeof(this."~c~"))"); 131 /** If the color has alpha. */ 132 enum bool hasAlpha = hasComponent!'a'; 133 134 135 /** Return the RGB tristimulus values as a tuple. 136 These will always be ordered (R, G, B). 137 Any color channels not present will be 0. */ 138 @property auto tristimulus() const 139 { 140 static if (hasComponent!'l') 141 { 142 return tuple(l, l, l); 143 } 144 else 145 { 146 static if (!hasComponent!'r') 147 enum r = ComponentType(0); 148 static if (!hasComponent!'g') 149 enum g = ComponentType(0); 150 static if (!hasComponent!'b') 151 enum b = ComponentType(0); 152 return tuple(r, g, b); 153 } 154 } 155 /// 156 unittest 157 { 158 // tristimulus returns tuple of R, G, B 159 static assert(BGR8(255, 128, 10).tristimulus == tuple(NormalizedInt!ubyte(255), NormalizedInt!ubyte(128), NormalizedInt!ubyte(10))); 160 } 161 162 /** Return the RGB tristimulus values + alpha as a tuple. 163 These will always be ordered (R, G, B, A). */ 164 @property auto tristimulusWithAlpha() const 165 { 166 static if (!hasAlpha) 167 enum a = defaultAlpha!ComponentType; 168 return tuple(tristimulus.expand, a); 169 } 170 /// 171 unittest 172 { 173 // tristimulusWithAlpha returns tuple of R, G, B, A 174 static assert(BGRA8(255, 128, 10, 80).tristimulusWithAlpha == tuple(NormalizedInt!ubyte(255), NormalizedInt!ubyte(128), NormalizedInt!ubyte(10), NormalizedInt!ubyte(80))); 175 } 176 177 /** Construct a color from RGB and optional alpha values. */ 178 this(ComponentType r, ComponentType g, ComponentType b, ComponentType a = defaultAlpha!ComponentType) 179 { 180 foreach (c; TypeTuple!("r","g","b","a")) 181 mixin(ComponentExpression!("this._ = _;", c, null)); 182 static if (canFind!(components, 'l')) 183 this.l = toGrayscale!(linear, colorSpace)(r, g, b); // ** Contentious? I this this is most useful 184 } 185 186 /** Construct a color from a luminance and optional alpha value. */ 187 this(ComponentType l, ComponentType a = defaultAlpha!ComponentType) 188 { 189 foreach (c; TypeTuple!("l","r","g","b")) 190 mixin(ComponentExpression!("this._ = l;", c, null)); 191 static if (canFind!(components, 'a')) 192 this.a = a; 193 } 194 195 static if (!isFloatingPoint!ComponentType_) 196 { 197 /** Construct a color from RGB and optional alpha values. */ 198 this(ComponentType.IntType r, ComponentType.IntType g, ComponentType.IntType b, ComponentType.IntType a = defaultAlpha!(ComponentType.IntType)) 199 { 200 foreach (c; TypeTuple!("r","g","b","a")) 201 mixin(ComponentExpression!("this._ = ComponentType(_);", c, null)); 202 static if (canFind!(components, 'l')) 203 this.l = toGrayscale!(linear, colorSpace)(ComponentType(r), ComponentType(g), ComponentType(b)); // ** Contentious? I this this is most useful 204 } 205 206 /** Construct a color from a luminance and optional alpha value. */ 207 this(ComponentType.IntType l, ComponentType.IntType a = defaultAlpha!(ComponentType.IntType)) 208 { 209 foreach (c; TypeTuple!("l","r","g","b")) 210 mixin(ComponentExpression!("this._ = ComponentType(l);", c, null)); 211 static if (canFind!(components, 'a')) 212 this.a = ComponentType(a); 213 } 214 } 215 216 /** 217 Cast to other color types. 218 219 This cast is a convenience which simply forwards the call to convertColor. 220 */ 221 Color opCast(Color)() const if (isColor!Color) 222 { 223 return convertColor!Color(this); 224 } 225 226 // comparison 227 bool opEquals(typeof(this) rh) const 228 { 229 // this is required to exclude 'x' components from equality comparisons 230 return tristimulusWithAlpha == rh.tristimulusWithAlpha; 231 } 232 233 /** Unary operators. */ 234 typeof(this) opUnary(string op)() const if (op == "+" || op == "-" || (op == "~" && is(ComponentType == NormalizedInt!U, U))) 235 { 236 Unqual!(typeof(this)) res = this; 237 foreach (c; AllComponents) 238 mixin(ComponentExpression!("res._ = #_;", c, op)); 239 return res; 240 } 241 /// 242 unittest 243 { 244 static assert(+UVW8(1,2,3) == UVW8(1,2,3)); 245 static assert(-UVW8(1,2,3) == UVW8(-1,-2,-3)); 246 247 static assert(~RGB8(1,2,3) == RGB8(0xFE,0xFD,0xFC)); 248 static assert(~UVW8(1,2,3) == UVW8(~1,~2,~3)); 249 } 250 251 /** Binary operators. */ 252 typeof(this) opBinary(string op)(typeof(this) rh) const if (op == "+" || op == "-" || op == "*") 253 { 254 Unqual!(typeof(this)) res = this; 255 foreach (c; AllComponents) 256 mixin(ComponentExpression!("res._ #= rh._;", c, op)); 257 return res; 258 } 259 /// 260 unittest 261 { 262 static assert(RGB8(10,20,30) + RGB8(4,5,6) == RGB8(14,25,36)); 263 static assert(UVW8(10,20,30) + UVW8(4,5,6) == UVW8(14,25,36)); 264 static assert(RGBAf32(10,20,30,40) + RGBAf32(4,5,6,7) == RGBAf32(14,25,36,47)); 265 266 static assert(RGB8(10,20,30) - RGB8(4,5,6) == RGB8(6,15,24)); 267 static assert(UVW8(10,20,30) - UVW8(4,5,6) == UVW8(6,15,24)); 268 static assert(RGBAf32(10,20,30,40) - RGBAf32(4,5,6,7) == RGBAf32(6,15,24,33)); 269 270 static assert(RGB8(10,20,30) * RGB8(128,128,128) == RGB8(5,10,15)); 271 static assert(UVW8(10,20,30) * UVW8(-64,-64,-64) == UVW8(-5,-10,-15)); 272 static assert(RGBAf32(10,20,30,40) * RGBAf32(0,1,2,3) == RGBAf32(0,20,60,120)); 273 } 274 275 /** Binary operators. */ 276 typeof(this) opBinary(string op, S)(S rh) const if (isColorScalarType!S && (op == "*" || op == "/" || op == "%" || op == "^^")) 277 { 278 Unqual!(typeof(this)) res = this; 279 foreach (c; AllComponents) 280 mixin(ComponentExpression!("res._ #= rh;", c, op)); 281 return res; 282 } 283 /// 284 unittest 285 { 286 static assert(RGB8(10,20,30) * 2 == RGB8(20,40,60)); 287 static assert(UVW8(10,20,30) * 2 == UVW8(20,40,60)); 288 static assert(RGBAf32(10,20,30,40) * 2 == RGBAf32(20,40,60,80)); 289 290 static assert(RGB8(10,20,30) / 2 == RGB8(5,10,15)); 291 static assert(UVW8(-10,-20,-30) / 2 == UVW8(-5,-10,-15)); 292 static assert(RGBAf32(10,20,30,40) / 2 == RGBAf32(5,10,15,20)); 293 294 static assert(RGB8(10,20,30) * 2.0 == RGB8(20,40,60)); 295 static assert(UVW8(10,20,30) * 2.0 == UVW8(20,40,60)); 296 static assert(RGBAf32(10,20,30,40) * 2.0 == RGBAf32(20,40,60,80)); 297 static assert(RGB8(10,20,30) * 0.5 == RGB8(5,10,15)); 298 static assert(UVW8(-10,-20,-30) * 0.5 == UVW8(-5,-10,-15)); 299 static assert(RGBAf32(5,10,15,20) * 0.5 == RGBAf32(2.5,5,7.5,10)); 300 301 static assert(RGB8(10,20,30) / 2.0 == RGB8(5,10,15)); 302 static assert(UVW8(-10,-20,-30) / 2.0 == UVW8(-5,-10,-15)); 303 static assert(RGBAf32(10,20,30,40) / 2.0 == RGBAf32(5,10,15,20)); 304 static assert(RGB8(10,20,30) / 0.5 == RGB8(20,40,60)); 305 static assert(UVW8(10,20,30) / 0.5 == UVW8(20,40,60)); 306 static assert(RGBAf32(10,20,30,40) / 0.5 == RGBAf32(20,40,60,80)); 307 } 308 309 /** Binary assignment operators. */ 310 ref typeof(this) opOpAssign(string op)(typeof(this) rh) if (op == "+" || op == "-" || op == "*") 311 { 312 foreach (c; AllComponents) 313 mixin(ComponentExpression!("_ #= rh._;", c, op)); 314 return this; 315 } 316 317 /** Binary assignment operators. */ 318 ref typeof(this) opOpAssign(string op, S)(S rh) if (isColorScalarType!S && (op == "*" || op == "/" || op == "%" || op == "^^")) 319 { 320 foreach (c; AllComponents) 321 mixin(ComponentExpression!("_ #= rh;", c, op)); 322 return this; 323 } 324 325 package: 326 327 alias ParentColor = XYZ!(FloatTypeFor!ComponentType); 328 329 static To convertColorImpl(To, From)(From color) if (isRGB!From && isRGB!To) 330 { 331 alias ToType = To.ComponentType; 332 alias FromType = From.ComponentType; 333 334 auto src = color.tristimulusWithAlpha; 335 336 static if (From.colorSpace == To.colorSpace && From.linear == To.linear) 337 { 338 // color space is the same, just do type conversion 339 return To(cast(ToType)src[0], cast(ToType)src[1], cast(ToType)src[2], cast(ToType)src[3]); 340 } 341 else 342 { 343 // unpack the working values 344 alias WorkType = WorkingType!(FromType, ToType); 345 WorkType r = cast(WorkType)src[0]; 346 WorkType g = cast(WorkType)src[1]; 347 WorkType b = cast(WorkType)src[2]; 348 349 static if (From.linear == false) 350 { 351 r = toLinear!(From.colorSpace)(r); 352 g = toLinear!(From.colorSpace)(g); 353 b = toLinear!(From.colorSpace)(b); 354 } 355 static if (From.colorSpace != To.colorSpace) 356 { 357 enum toXYZ = rgbToXyzMatrix(From.colorSpaceDesc!WorkType); 358 enum toRGB = xyzToRgbMatrix(To.colorSpaceDesc!WorkType); 359 enum mat = multiply(toXYZ, toRGB); 360 WorkType[3] v = multiply(mat, [r, g, b]); 361 r = v[0]; g = v[1]; b = v[2]; 362 } 363 static if (To.linear == false) 364 { 365 r = toGamma!(To.colorSpace)(r); 366 g = toGamma!(To.colorSpace)(g); 367 b = toGamma!(To.colorSpace)(b); 368 } 369 370 // convert and return the output 371 static if (To.hasAlpha) 372 return To(cast(ToType)r, cast(ToType)g, cast(ToType)b, cast(ToType)src[3]); 373 else 374 return To(cast(ToType)r, cast(ToType)g, cast(ToType)b); 375 } 376 } 377 unittest 378 { 379 // test RGB format conversions 380 alias UnsignedRGB = RGB!("rgb", ubyte); 381 alias SignedRGBX = RGB!("rgbx", byte); 382 alias FloatRGBA = RGB!("rgba", float); 383 384 static assert(convertColorImpl!(UnsignedRGB)(SignedRGBX(0x20,0x30,-10)) == UnsignedRGB(0x40,0x60,0)); 385 static assert(convertColorImpl!(UnsignedRGB)(FloatRGBA(1,0.5,0,1)) == UnsignedRGB(0xFF,0x80,0)); 386 static assert(convertColorImpl!(FloatRGBA)(UnsignedRGB(0xFF,0x80,0)) == FloatRGBA(1,float(0x80)/float(0xFF),0,0)); 387 static assert(convertColorImpl!(FloatRGBA)(SignedRGBX(127,-127,-128)) == FloatRGBA(1,-1,-1,0)); 388 389 static assert(convertColorImpl!(UnsignedRGB)(convertColorImpl!(FloatRGBA)(UnsignedRGB(0xFF,0x80,0))) == UnsignedRGB(0xFF,0x80,0)); 390 391 // test greyscale conversion 392 alias UnsignedL = RGB!("l", ubyte); 393 static assert(cast(UnsignedL)UnsignedRGB(0xFF,0x20,0x40) == UnsignedL(82)); 394 395 // TODO: we can't test this properly since DMD can't CTFE the '^^' operator! >_< 396 397 alias sRGBA = RGB!("rgba", ubyte, false, RGBColorSpace.sRGB); 398 399 // test linear conversion 400 alias lRGBA = RGB!("rgba", ushort, true, RGBColorSpace.sRGB); 401 assert(convertColorImpl!(lRGBA)(sRGBA(0xFF, 0xFF, 0xFF, 0xFF)) == lRGBA(0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF)); 402 403 // test gamma conversion 404 alias gRGBA = RGB!("rgba", byte, false, RGBColorSpace.sRGB_Gamma2_2); 405 assert(convertColorImpl!(gRGBA)(sRGBA(0xFF, 0x80, 0x01, 0xFF)) == gRGBA(0x7F, 0x3F, 0x03, 0x7F)); 406 } 407 408 static To convertColorImpl(To, From)(From color) if (isRGB!From && isXYZ!To) 409 { 410 alias ToType = To.ComponentType; 411 alias FromType = From.ComponentType; 412 alias WorkType = WorkingType!(FromType, ToType); 413 414 // unpack the working values 415 auto src = color.tristimulus; 416 WorkType r = cast(WorkType)src[0]; 417 WorkType g = cast(WorkType)src[1]; 418 WorkType b = cast(WorkType)src[2]; 419 420 static if (From.linear == false) 421 { 422 r = toLinear!(From.colorSpace)(r); 423 g = toLinear!(From.colorSpace)(g); 424 b = toLinear!(From.colorSpace)(b); 425 } 426 427 // transform to XYZ 428 enum toXYZ = rgbToXyzMatrix(From.colorSpaceDesc!WorkType); 429 WorkType[3] v = multiply(toXYZ, [r, g, b]); 430 return To(v[0], v[1], v[2]); 431 } 432 unittest 433 { 434 // TODO: needs approx == 435 } 436 437 static To convertColorImpl(To, From)(From color) if (isXYZ!From && isRGB!To) 438 { 439 alias ToType = To.ComponentType; 440 alias FromType = From.ComponentType; 441 alias WorkType = WorkingType!(FromType, ToType); 442 443 enum toRGB = xyzToRgbMatrix(To.colorSpaceDesc!WorkType); 444 WorkType[3] v = multiply(toRGB, [ WorkType(color.X), WorkType(color.Y), WorkType(color.Z) ]); 445 446 static if (To.linear == false) 447 { 448 v[0] = toGamma!(To.colorSpace)(v[0]); 449 v[1] = toGamma!(To.colorSpace)(v[1]); 450 v[2] = toGamma!(To.colorSpace)(v[2]); 451 } 452 453 return To(cast(ToType)v[0], cast(ToType)v[1], cast(ToType)v[2]); 454 } 455 unittest 456 { 457 // TODO: needs approx == 458 } 459 460 private: 461 alias AllComponents = TypeTuple!("l","r","g","b","a"); 462 } 463 464 465 /** Convert a value from gamma compressed space to linear. */ 466 T toLinear(RGBColorSpace src, T)(T v) if (isFloatingPoint!T) 467 { 468 enum ColorSpace = rgbColorSpaceDefs!T[src]; 469 return ColorSpace.toLinear(v); 470 } 471 /** Convert a value to gamma compressed space. */ 472 T toGamma(RGBColorSpace src, T)(T v) if (isFloatingPoint!T) 473 { 474 enum ColorSpace = rgbColorSpaceDefs!T[src]; 475 return ColorSpace.toGamma(v); 476 } 477 478 /** Convert a color to linear space. */ 479 auto toLinear(C)(C color) if (isRGB!C) 480 { 481 return cast(RGB!(C.components, C.ComponentType, true, C.colorSpace))color; 482 } 483 /** Convert a color to gamma space. */ 484 auto toGamma(C)(C color) if (isRGB!C) 485 { 486 return cast(RGB!(C.components, C.ComponentType, false, C.colorSpace))color; 487 } 488 489 490 package: 491 492 T toGrayscale(bool linear, RGBColorSpace colorSpace = RGBColorSpace.sRGB, T)(T r, T g, T b) pure if (isFloatingPoint!T) 493 { 494 static if (linear) 495 { 496 // calculate the luminance (Y) value correctly by multiplying the Y row of the XYZ matrix with the color 497 enum YAxis = rgbColorSpaceDef!T(colorSpace).rgbToXyzMatrix()[1]; 498 return YAxis[0]*r + YAxis[1]*g + YAxis[2]*b; 499 } 500 else static if (colorSpace == RGBColorSpace.Colorimetry || 501 colorSpace == RGBColorSpace.NTSC || 502 colorSpace == RGBColorSpace.NTSC_J || 503 colorSpace == RGBColorSpace.PAL_SECAM) 504 { 505 // For color spaces which are used in standard color TV and video systems such as PAL/SECAM, and 506 // NTSC, a nonlinear luma component (Y') is calculated directly from gamma-compressed primary 507 // intensities as a weighted sum, which can be calculated quickly without the gamma expansion and 508 // compression used in colorimetric grayscale calculations. 509 // The Rec.601 luma (Y') component is computed as: 510 return T(0.299)*r + T(0.587)*g + T(0.114)*b; 511 } 512 else static if (colorSpace == RGBColorSpace.HDTV) 513 { 514 // The Rec.709 standard used for HDTV uses different color coefficients. 515 // These happen to be the same as sRGB, but applied to the gamma compressed signal direcetly. 516 return T(0.2126)*r + T(0.7152)*g + T(0.0722)*b; 517 } 518 else 519 { 520 // Edge-case: What to do?! Approximate, or perform gamma conversions? 521 // The TV standards have defined approximations, so let's continue to roll with that pattern. 522 // We'll continue the Rec.709 pattern, except using appropriate coefficients for the color space. 523 enum YAxis = rgbColorSpaceDef!T(colorSpace).rgbToXyzMatrix()[1]; 524 return YAxis[0]*r + YAxis[1]*g + YAxis[2]*b; 525 } 526 } 527 T toGrayscale(bool linear, RGBColorSpace colorSpace = RGBColorSpace.sRGB, T)(T r, T g, T b) pure if (is(T == NormalizedInt!U, U)) 528 { 529 alias F = FloatTypeFor!T; 530 return T(toGrayscale!(linear, colorSpace)(cast(F)r, cast(F)g, cast(F)b)); 531 } 532 533 534 // helpers to parse color components from color component string 535 template canFind(string s, char c) 536 { 537 static if (s.length == 0) 538 enum canFind = false; 539 else 540 enum canFind = s[0] == c || canFind!(s[1..$], c); 541 } 542 template allIn(string s, string chars) 543 { 544 static if (chars.length == 0) 545 enum allIn = true; 546 else 547 enum allIn = canFind!(s, chars[0]) && allIn!(s, chars[1..$]); 548 } 549 template anyIn(string s, string chars) 550 { 551 static if (chars.length == 0) 552 enum anyIn = false; 553 else 554 enum anyIn = canFind!(s, chars[0]) || anyIn!(s, chars[1..$]); 555 } 556 template notIn(string s, string chars) 557 { 558 static if (chars.length == 0) 559 enum notIn = char(0); 560 else static if (!canFind!(s, chars[0])) 561 enum notIn = chars[0]; 562 else 563 enum notIn = notIn!(s, chars[1..$]); 564 } 565 566 unittest 567 { 568 static assert(canFind!("string", 'i')); 569 static assert(!canFind!("string", 'x')); 570 static assert(allIn!("string", "sgi")); 571 static assert(!allIn!("string", "sgix")); 572 static assert(anyIn!("string", "sx")); 573 static assert(!anyIn!("string", "x")); 574 }