1 module magicalrainbows.formats; 2 3 import magicalrainbows.utils; 4 5 import std.algorithm; 6 import std.bitmanip; 7 import std.conv; 8 import std.range; 9 10 11 struct BGR555 { //XBBBBBGG GGGRRRRR 12 enum redSize = 5; 13 enum greenSize = 5; 14 enum blueSize = 5; 15 enum alphaSize = 0; 16 mixin colourConstructors; 17 mixin(bitfields!( 18 uint, "red", redSize, 19 uint, "green", greenSize, 20 uint, "blue", blueSize, 21 bool, "padding", 1)); 22 } 23 24 @safe pure unittest { 25 with(BGR555(AnalogRGB(1.0, 0.5, 0.0))) { 26 assert(red == 31); 27 assert(green == 15); 28 assert(blue == 0); 29 } 30 } 31 32 struct BGR565 { //BBBBBGGG GGGRRRRR 33 enum redSize = 5; 34 enum greenSize = 6; 35 enum blueSize = 5; 36 enum alphaSize = 0; 37 mixin colourConstructors; 38 mixin(bitfields!( 39 uint, "red", redSize, 40 uint, "green", greenSize, 41 uint, "blue", blueSize)); 42 } 43 44 struct BGR222 { //00BBGGRR 45 enum redSize = 2; 46 enum greenSize = 2; 47 enum blueSize = 2; 48 enum alphaSize = 0; 49 mixin colourConstructors; 50 mixin(bitfields!( 51 uint, "red", redSize, 52 uint, "green", greenSize, 53 uint, "blue", blueSize, 54 ubyte, "padding", 2)); 55 } 56 struct BGR333MD { //0000BBB0 GGG0RRR0 57 enum redSize = 3; 58 enum greenSize = 3; 59 enum blueSize = 3; 60 enum alphaSize = 0; 61 mixin colourConstructors; 62 mixin(bitfields!( 63 ubyte, "padding0", 1, 64 uint, "red", redSize, 65 ubyte, "padding1", 1, 66 uint, "green", greenSize, 67 ubyte, "padding2", 1, 68 uint, "blue", blueSize, 69 ubyte, "padding3", 4)); 70 } 71 72 struct RGB888 { //RRRRRRRR GGGGGGGG BBBBBBBB 73 enum redSize = 8; 74 enum greenSize = 8; 75 enum blueSize = 8; 76 enum alphaSize = 0; 77 mixin colourConstructors; 78 ubyte red; 79 ubyte green; 80 ubyte blue; 81 } 82 83 struct RGBA8888 { //RRRRRRRR GGGGGGGG BBBBBBBB AAAAAAAA 84 enum redSize = 8; 85 enum greenSize = 8; 86 enum blueSize = 8; 87 enum alphaSize = 8; 88 mixin colourConstructors; 89 ubyte red; 90 ubyte green; 91 ubyte blue; 92 ubyte alpha; 93 } 94 @safe pure unittest { 95 with(RGBA8888(AnalogRGBA(1.0, 0.5, 0.0, 0.0))) { 96 assert(red == 255); 97 assert(green == 127); 98 assert(blue == 0); 99 assert(alpha == 0); 100 } 101 with(RGBA8888(255, 128, 0, 0)) { 102 assert(red == 255); 103 assert(green == 128); 104 assert(blue == 0); 105 assert(alpha == 0); 106 } 107 } 108 109 struct AnalogRGB { 110 real red; 111 real green; 112 real blue; 113 } 114 115 struct AnalogRGBA { 116 real red; 117 real green; 118 real blue; 119 real alpha; 120 } 121 122 struct HSV { 123 real hue; 124 real saturation; 125 real value; 126 @safe invariant { 127 assert(hue >= 0); 128 assert(saturation >= 0); 129 assert(value >= 0); 130 assert(hue <= 1.0); 131 assert(saturation <= 1.0); 132 assert(value <= 1.0); 133 } 134 } 135 136 auto toHSV(Format)(Format input) if (isColourFormat!Format) { 137 import std.algorithm.comparison : max, min; 138 import std.math : approxEqual; 139 HSV result; 140 const red = input.redFP; 141 const green = input.greenFP; 142 const blue = input.blueFP; 143 const minimum = min(red, green, blue); 144 const maximum = max(red, green, blue); 145 const delta = maximum - minimum; 146 147 result.value = maximum; 148 if (delta < 0.00001) 149 { 150 result.saturation = 0; 151 result.hue = 0; 152 return result; 153 } 154 if (maximum > 0.0) { 155 result.saturation = delta / maximum; 156 } 157 158 if (approxEqual(red, maximum)) { 159 result.hue = (green - blue) / delta; //yellow, magenta 160 } else if (approxEqual(green, maximum)) { 161 result.hue = 2.0 + (blue - red) / delta; //cyan, yellow 162 } else { 163 result.hue = 4.0 + (red - green) / delta; //magenta, cyan 164 } 165 166 result.hue /= 6.0; 167 if (result.hue < 0.0) { 168 result.hue += 1.0; 169 } 170 assert(result.hue >= 0.0); 171 172 return result; 173 } 174 /// 175 @safe unittest { 176 import std.math : approxEqual; 177 with(RGB888(0, 0, 0).toHSV) { 178 assert(hue == 0); 179 assert(saturation == 0); 180 assert(value == 0); 181 } 182 with(RGB888(0, 128, 192).toHSV) { 183 assert(hue.approxEqual(0.5555555)); 184 assert(saturation.approxEqual(1.0)); 185 assert(value.approxEqual(0.752941)); 186 } 187 with(RGB888(255, 255, 0).toHSV) { 188 assert(hue.approxEqual(0.166667)); 189 assert(saturation.approxEqual(1.0)); 190 assert(value.approxEqual(1.0)); 191 } 192 with(RGB888(255, 0, 0).toHSV) { 193 assert(hue.approxEqual(0.0)); 194 assert(saturation.approxEqual(1.0)); 195 assert(value.approxEqual(1.0)); 196 } 197 with(RGB888(255, 0, 255).toHSV) { 198 assert(hue.approxEqual(0.833333)); 199 assert(saturation.approxEqual(1.0)); 200 assert(value.approxEqual(1.0)); 201 } 202 with(RGB888(0, 255, 0).toHSV) { 203 assert(hue.approxEqual(0.333333)); 204 assert(saturation.approxEqual(1.0)); 205 assert(value.approxEqual(1.0)); 206 } 207 with(RGB888(0, 0, 255).toHSV) { 208 assert(hue.approxEqual(0.666667)); 209 assert(saturation.approxEqual(1.0)); 210 assert(value.approxEqual(1.0)); 211 } 212 } 213 214 auto toRGB(Format = RGB888)(HSV input) @safe if (isColourFormat!Format) { 215 if(input.saturation <= 0.0) { 216 return Format(AnalogRGB(input.value, input.value, input.value)); 217 } 218 real hh = input.hue * 6.0; 219 if(hh > 6.0) { 220 hh = 0.0; 221 } 222 auto i = cast(long)hh; 223 real ff = hh - i; 224 real p = input.value * (1.0 - input.saturation); 225 real q = input.value * (1.0 - (input.saturation * ff)); 226 real t = input.value * (1.0 - (input.saturation * (1.0 - ff))); 227 228 assert(p <= 1.0); 229 assert(q <= 1.0); 230 assert(t <= 1.0); 231 AnalogRGB rgb; 232 switch(i) { 233 case 0: 234 rgb = AnalogRGB(input.value, t, p); 235 break; 236 case 1: 237 rgb = AnalogRGB(q, input.value, p); 238 break; 239 case 2: 240 rgb = AnalogRGB(p, input.value, t); 241 break; 242 case 3: 243 rgb = AnalogRGB(p, q, input.value); 244 break; 245 case 4: 246 rgb = AnalogRGB(t, p, input.value); 247 break; 248 case 5: 249 default: 250 rgb = AnalogRGB(input.value, p, q); 251 break; 252 } 253 return Format(rgb); 254 } 255 /// 256 @safe unittest { 257 with(HSV(0, 0, 0).toRGB!RGB888) { 258 assert(red == 0); 259 assert(green == 0); 260 assert(blue == 0); 261 } 262 with(HSV(0, 0, 0.5).toRGB!RGB888) { 263 assert(red == 127); 264 assert(green == 127); 265 assert(blue == 127); 266 } 267 with(HSV(0.5555555, 1.0, 0.752941).toRGB!RGB888) { 268 assert(red == 0); 269 assert(green == 128); 270 assert(blue == 191); 271 } 272 with(HSV(0.166666667, 1.0, 1.0).toRGB!RGB888) { 273 assert(red == 254); 274 assert(green == 255); 275 assert(blue == 0); 276 } 277 with(HSV(0.0, 1.0, 1.0).toRGB!RGB888) { 278 assert(red == 255); 279 assert(green == 0); 280 assert(blue == 0); 281 } 282 with(HSV(0.83333333, 1.0, 1.0).toRGB!RGB888) { 283 assert(red == 254); 284 assert(green == 0); 285 assert(blue == 255); 286 } 287 with(HSV(0.33333333, 1.0, 1.0).toRGB!RGB888) { 288 assert(red == 0); 289 assert(green == 255); 290 assert(blue == 0); 291 } 292 with(HSV(0.66666667, 1.0, 1.0).toRGB!RGB888) { 293 assert(red == 0); 294 assert(green == 0); 295 assert(blue == 255); 296 } 297 with(HSV(0.41666667, 1.0, 1.0).toRGB!RGB888) { 298 assert(red == 0); 299 assert(green == 255); 300 assert(blue == 127); 301 } 302 with(HSV(0.91666667, 1.0, 1.0).toRGB!RGB888) { 303 assert(red == 255); 304 assert(green == 0); 305 assert(blue == 127); 306 } 307 } 308 309 struct ColourPair(FG, BG) if (isColourFormat!FG && isColourFormat!BG) { 310 FG foreground; 311 BG background; 312 auto contrast() const @safe pure { 313 import magicalrainbows.properties : contrast; 314 return contrast(foreground, background); 315 } 316 bool meetsWCAGAACriteria() const @safe pure { 317 return contrast >= 4.5; 318 } 319 bool meetsWCAGAAACriteria() const @safe pure { 320 return contrast >= 7.0; 321 } 322 } 323 324 auto colourPair(FG, BG)(FG foreground, BG background) if (isColourFormat!FG && isColourFormat!BG) { 325 return ColourPair!(FG, BG)(foreground, background); 326 } 327 /// 328 @safe pure unittest { 329 import std.math : approxEqual; 330 with(colourPair(RGB888(0, 0, 0), RGB888(255, 255, 255))) { 331 assert(contrast.approxEqual(21.0)); 332 assert(meetsWCAGAACriteria); 333 assert(meetsWCAGAAACriteria); 334 } 335 with(colourPair(RGB888(255, 255, 255), RGB888(255, 255, 255))) { 336 assert(contrast.approxEqual(1.0)); 337 assert(!meetsWCAGAACriteria); 338 assert(!meetsWCAGAAACriteria); 339 } 340 with(colourPair(RGB888(0, 128, 255), RGB888(0, 0, 0))) { 341 assert(contrast.approxEqual(5.5316)); 342 assert(meetsWCAGAACriteria); 343 assert(!meetsWCAGAAACriteria); 344 } 345 with(colourPair(BGR555(0, 16, 31), RGB888(0, 0, 0))) { 346 assert(contrast.approxEqual(5.7236)); 347 assert(meetsWCAGAACriteria); 348 assert(!meetsWCAGAAACriteria); 349 } 350 } 351 352 auto convert(To, From)(From from) if (isColourFormat!From && isColourFormat!To) { 353 static if (is(To == From)) { 354 return from; 355 } else { 356 To output; 357 static if (hasRed!From && hasRed!To) { 358 output.red = colourConvert!(typeof(output.red), To.redSize, From.redSize)(from.red); 359 } 360 static if (hasGreen!From && hasGreen!To) { 361 output.green = colourConvert!(typeof(output.green), To.greenSize, From.greenSize)(from.green); 362 } 363 static if (hasBlue!From && hasBlue!To) { 364 output.blue = colourConvert!(typeof(output.blue), To.blueSize, From.blueSize)(from.blue); 365 } 366 static if (hasAlpha!From && hasAlpha!To) { 367 output.alpha = colourConvert!(typeof(output.alpha), To.alphaSize, From.alphaSize)(from.alpha); 368 } else static if (hasAlpha!To) { 369 output.alpha = output.alpha.max; 370 } 371 return output; 372 } 373 } 374 /// 375 @safe pure unittest { 376 assert(BGR555(31,31,31).convert!RGB888 == RGB888(248, 248, 248)); 377 assert(BGR555(0, 0, 0).convert!RGB888 == RGB888(0, 0, 0)); 378 assert(RGB888(248, 248, 248).convert!BGR555 == BGR555(31,31,31)); 379 assert(RGB888(0, 0, 0).convert!BGR555 == BGR555(0, 0, 0)); 380 } 381 382 Format fromHex(Format = RGB888)(const string colour) @safe pure if (isColourFormat!Format) { 383 Format output; 384 string tmpStr = colour[]; 385 if (colour.empty) { 386 throw new Exception("Cannot parse an empty string"); 387 } 388 if (tmpStr.front == '#') { 389 tmpStr.popFront(); 390 } 391 enum alphaAdjustment = hasAlpha!Format ? 1 : 0; 392 if (tmpStr.length == 3 + alphaAdjustment) { 393 auto tmp = tmpStr[0].repeat(2); 394 output.red = tmp.parse!ubyte(16); 395 tmp = tmpStr[1].repeat(2); 396 output.green = tmp.parse!ubyte(16); 397 tmp = tmpStr[2].repeat(2); 398 output.blue = tmp.parse!ubyte(16); 399 static if (hasAlpha!Format) { 400 tmp = tmpStr[3].repeat(2); 401 output.alpha = tmp.parse!ubyte(16); 402 } 403 } else if (tmpStr.length == (3 + alphaAdjustment) * 2) { 404 auto tmp = tmpStr[0..2]; 405 output.red = tmp.parse!ubyte(16); 406 tmp = tmpStr[2..4]; 407 output.green = tmp.parse!ubyte(16); 408 tmp = tmpStr[4..6]; 409 output.blue = tmp.parse!ubyte(16); 410 static if (hasAlpha!Format) { 411 tmp = tmpStr[6..8]; 412 output.alpha = tmp.parse!ubyte(16); 413 } 414 } 415 return output; 416 } 417 /// 418 @safe pure unittest { 419 assert("#000000".fromHex == RGB888(0, 0, 0)); 420 assert("#FFFFFF".fromHex == RGB888(255, 255, 255)); 421 assert("FFFFFF".fromHex == RGB888(255, 255, 255)); 422 assert("#FFF".fromHex == RGB888(255, 255, 255)); 423 assert("#888".fromHex == RGB888(0x88, 0x88, 0x88)); 424 assert("888".fromHex == RGB888(0x88, 0x88, 0x88)); 425 assert("#FFFFFFFF".fromHex!RGBA8888 == RGBA8888(255, 255, 255, 255)); 426 assert("#FFFF".fromHex!RGBA8888 == RGBA8888(255, 255, 255, 255)); 427 } 428 429 //TODO: can we express these with fractions instead? 430 431 //Assumptions: 432 // R,G,B,Y [0, 255] 433 // Pb,Pr [-127.5, 127.5] 434 enum YPbPrSDTVToRGBMatrix = [ 435 [1.0, 0.0, 1.402], 436 [1.0, -0.344, -0.714], 437 [1.0, 1.772, 0.0], 438 ]; 439 //Assumptions: 440 // R,G,B,Y [0, 255] 441 // Pb,Pr [-127.5, 127.5] 442 enum YPbPrHDTVToRGBMatrix = [ 443 [1.0, 0.0, 1.575], 444 [1.0, -0.187, -0.468], 445 [1.0, 1.856, 0.0], 446 ]; 447 448 //Assumptions: 449 // R,G,B,Y [0, 255] 450 // Pb,Pr [-127.5, 127.5] 451 enum RGBToYPbPrSDTVMatrix = [ 452 [0.299, 0.587, 0.114], 453 [-0.169, -0.331, 0.5], 454 [0.5, -0.419, -0.081], 455 ]; 456 457 //Assumptions: 458 // R,G,B,Y [0, 255] 459 // Pb,Pr [-127.5, 127.5] 460 enum RGBToYPbPrHDTVMatrix = [ 461 [0.213, 0.715, 0.072], 462 [-0.115, -0.385, 0.5], 463 [0.5, -0.454, -0.046], 464 ]; 465 466 struct YPbPr { 467 double Y; 468 double Pb; 469 double Pr; 470 } 471 472 //Assumptions: 473 // R,G,B,Y [0, 1] 474 // U [-0.436, 0.436] 475 // V [-0.615, 0.615] 476 enum RGBToYUVMatrix = [ 477 [0.299, 0.587, 0.114], 478 [-0.147, -0.289, 0.436], 479 [0.615, -0.515, -0.100] 480 ]; 481 482 //Assumptions: 483 // R,G,B,Y [0, 1] 484 // U [-0.436, 0.436] 485 // V [-0.615, 0.615] 486 enum YUVToRGBMatrix = [ 487 [1.0, 0.0, 1.140], 488 [1.0, -0.395, -0.581], 489 [1.0, 2.032, 0.0], 490 ]; 491 492 struct YUV { 493 double Y; 494 double Cb; 495 double Cr; 496 } 497 498 enum YCbCrSDTVVector = [[16], [128], [128]]; 499 alias YCbCrHDTVVector = YCbCrSDTVVector; 500 enum YCbCrFullRangeVector = [[0], [128], [128]]; 501 502 //Assumptions: 503 // R,G,B [0, 255] 504 // Y [16, 235] 505 // Cb,Cr [16, 240] 506 enum YCbCrSDTVToRGBMatrix = [ 507 [1.164, 0.0, 1.596], 508 [1.164, -0.392, -0.813], 509 [1.164, 2.017, 0.0], 510 ]; 511 512 //Assumptions: 513 // R,G,B [0, 255] 514 // Y [16, 235] 515 // Cb,Cr [16, 240] 516 enum YCbCrHDTVToRGBMatrix = [ 517 [1.164, 0.0, 1.793], 518 [1.164, -0.213, -0.533], 519 [1.164, 2.112, 0.0], 520 ]; 521 522 //Assumptions: 523 // R,G,B,Y,Cb,Cr [0, 255] 524 enum YCbCrFullRangeToRGBMatrix = [ 525 [1.0, 0.0, 1.4], 526 [1.0, -0.343, -0.711], 527 [1.0, 1.765, 0.0], 528 ]; 529 530 //Assumptions: 531 // R,G,B [0, 255] 532 // Y [16, 235] 533 // Cb,Cr [16, 240] 534 enum RGBToYCbCrSDTVMatrix = [ 535 [0.257, 0.504, 0.098], 536 [-0.148, -0.291, 0.439], 537 [0.439, -0.368, -0.071], 538 ]; 539 540 //Assumptions: 541 // R,G,B [0, 255] 542 // Y [16, 235] 543 // Cb,Cr [16, 240] 544 enum RGBToYCbCrHDTVMatrix = [ 545 [0.183, 0.614, 0.062], 546 [-0.101, -0.339, 0.439], 547 [0.439, -0.399, -0.040], 548 ]; 549 550 //Assumptions: 551 // R,G,B,Y,Cb,Cr [0, 255] 552 enum RGBToYCbCrFullRangeMatrix = [ 553 [0.299, 0.587, 0.114], 554 [-0.169, -0.331, 0.500], 555 [0.500, -0.419, -0.081], 556 ]; 557 558 struct YCbCr { 559 double Y; 560 double Cb; 561 double Cr; 562 }