1 /++ 2 Base module for working with colors and in-memory image pixmaps. 3 4 Also has various basic data type definitions that are generally 5 useful with images like [Point], [Size], and [Rectangle]. 6 +/ 7 module arsd.color; 8 9 @safe: 10 11 // importing phobos explodes the size of this code 10x, so not doing it. 12 13 private { 14 double toInternal(T)(scope const(char)[] s) { 15 double accumulator = 0.0; 16 size_t i = s.length; 17 foreach(idx, c; s) { 18 if(c >= '0' && c <= '9') { 19 accumulator *= 10; 20 accumulator += c - '0'; 21 } else if(c == '.') { 22 i = idx + 1; 23 break; 24 } else { 25 string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make double from "; 26 wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s; 27 throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute); 28 } 29 } 30 31 double accumulator2 = 0.0; 32 double count = 1; 33 foreach(c; s[i .. $]) { 34 if(c >= '0' && c <= '9') { 35 accumulator2 *= 10; 36 accumulator2 += c - '0'; 37 count *= 10; 38 } else { 39 string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make double from "; 40 wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s; 41 throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute); 42 } 43 } 44 45 return accumulator + accumulator2 / count; 46 } 47 48 package(arsd) @trusted 49 string toInternal(T)(int a) { 50 if(a == 0) 51 return "0"; 52 char[] ret; 53 bool neg; 54 if(a < 0) { 55 neg = true; 56 a = -a; 57 } 58 while(a) { 59 ret ~= (a % 10) + '0'; 60 a /= 10; 61 } 62 for(int i = 0; i < ret.length / 2; i++) { 63 char c = ret[i]; 64 ret[i] = ret[$ - i - 1]; 65 ret[$ - i - 1] = c; 66 } 67 if(neg) 68 ret = "-" ~ ret; 69 70 return cast(string) ret; 71 } 72 string toInternal(T)(double a) { 73 // a simplifying assumption here is the fact that we only use this in one place: toInternal!string(cast(double) a / 255) 74 // thus we know this will always be between 0.0 and 1.0, inclusive. 75 if(a <= 0.0) 76 return "0.0"; 77 if(a >= 1.0) 78 return "1.0"; 79 string ret = "0."; 80 // I wonder if I can handle round off error any better. Phobos does, but that isn't worth 100 KB of code. 81 int amt = cast(int)(a * 1000); 82 return ret ~ toInternal!string(amt); 83 } 84 85 nothrow @safe @nogc pure 86 double absInternal(double a) { return a < 0 ? -a : a; } 87 nothrow @safe @nogc pure 88 double minInternal(double a, double b, double c) { 89 auto m = a; 90 if(b < m) m = b; 91 if(c < m) m = c; 92 return m; 93 } 94 nothrow @safe @nogc pure 95 double maxInternal(double a, double b, double c) { 96 auto m = a; 97 if(b > m) m = b; 98 if(c > m) m = c; 99 return m; 100 } 101 nothrow @safe @nogc pure 102 bool startsWithInternal(in char[] a, in char[] b) { 103 return (a.length >= b.length && a[0 .. b.length] == b); 104 } 105 void splitInternal(scope inout(char)[] a, char c, scope void delegate(int, scope inout(char)[]) @safe dg) { 106 int count; 107 size_t previous = 0; 108 foreach(i, char ch; a) { 109 if(ch == c) { 110 dg(count++, a[previous .. i]); 111 previous = i + 1; 112 } 113 } 114 if(previous != a.length) 115 dg(count++, a[previous .. $]); 116 } 117 nothrow @safe @nogc pure 118 inout(char)[] stripInternal(return inout(char)[] s) { 119 foreach(i, char c; s) 120 if(c != ' ' && c != '\t' && c != '\n') { 121 s = s[i .. $]; 122 break; 123 } 124 for(int a = cast(int)(s.length - 1); a > 0; a--) { 125 char c = s[a]; 126 if(c != ' ' && c != '\t' && c != '\n') { 127 s = s[0 .. a + 1]; 128 break; 129 } 130 } 131 132 return s; 133 } 134 } 135 136 // done with mini-phobos 137 138 /// Represents an RGBA color 139 struct Color { 140 141 @system static Color fromJsVar(T)(T v) { // it is a template so i don't have to actually import arsd.jsvar... 142 return Color.fromString(v.get!string); 143 } 144 145 @safe: 146 /++ 147 The color components are available as a static array, individual bytes, and a uint inside this union. 148 149 Since it is anonymous, you can use the inner members' names directly. 150 +/ 151 union { 152 ubyte[4] components; /// [r, g, b, a] 153 154 /// Holder for rgba individual components. 155 struct { 156 ubyte r; /// red 157 ubyte g; /// green 158 ubyte b; /// blue 159 ubyte a; /// alpha. 255 == opaque 160 } 161 162 uint asUint; /// The components as a single 32 bit value (beware of endian issues!) 163 } 164 165 /++ 166 Returns a value compatible with [https://docs.microsoft.com/en-us/windows/win32/gdi/colorref|a Win32 COLORREF]. 167 168 Please note that the alpha value is lost in translation. 169 170 History: 171 Added November 27, 2021 (dub v10.4) 172 See_Also: 173 [fromWindowsColorRef] 174 +/ 175 nothrow pure @nogc 176 uint asWindowsColorRef() { 177 uint cr; 178 cr |= b << 16; 179 cr |= g << 8; 180 cr |= r; 181 return cr; 182 } 183 184 /++ 185 Constructs a Color from [https://docs.microsoft.com/en-us/windows/win32/gdi/colorref|a Win32 COLORREF]. 186 187 History: 188 Added November 27, 2021 (dub v10.4) 189 See_Also: 190 [asWindowsColorRef] 191 +/ 192 nothrow pure @nogc 193 static Color fromWindowsColorRef(uint cr) { 194 return Color(cr & 0xff, (cr >> 8) & 0xff, (cr >> 16) & 0xff); 195 } 196 197 /++ 198 Like the constructor, but this makes sure they are in range before casting. If they are out of range, it saturates: anything less than zero becomes zero and anything greater than 255 becomes 255. 199 +/ 200 nothrow pure @nogc 201 static Color fromIntegers(int red, int green, int blue, int alpha = 255) { 202 return Color(clampToByte(red), clampToByte(green), clampToByte(blue), clampToByte(alpha)); 203 } 204 205 /// Construct a color with the given values. They should be in range 0 <= x <= 255, where 255 is maximum intensity and 0 is minimum intensity. 206 nothrow pure @nogc 207 this(int red, int green, int blue, int alpha = 255) { 208 this.r = cast(ubyte) red; 209 this.g = cast(ubyte) green; 210 this.b = cast(ubyte) blue; 211 this.a = cast(ubyte) alpha; 212 } 213 214 /// Static convenience functions for common color names 215 nothrow pure @nogc 216 static Color transparent() { return Color(0, 0, 0, 0); } 217 /// Ditto 218 nothrow pure @nogc 219 static Color white() { return Color(255, 255, 255); } 220 /// Ditto 221 nothrow pure @nogc 222 static Color gray() { return Color(128, 128, 128); } 223 /// Ditto 224 nothrow pure @nogc 225 static Color black() { return Color(0, 0, 0); } 226 /// Ditto 227 nothrow pure @nogc 228 static Color red() { return Color(255, 0, 0); } 229 /// Ditto 230 nothrow pure @nogc 231 static Color green() { return Color(0, 255, 0); } 232 /// Ditto 233 nothrow pure @nogc 234 static Color blue() { return Color(0, 0, 255); } 235 /// Ditto 236 nothrow pure @nogc 237 static Color yellow() { return Color(255, 255, 0); } 238 /// Ditto 239 nothrow pure @nogc 240 static Color teal() { return Color(0, 255, 255); } 241 /// Ditto 242 nothrow pure @nogc 243 static Color purple() { return Color(128, 0, 128); } 244 /// Ditto 245 nothrow pure @nogc 246 static Color magenta() { return Color(255, 0, 255); } 247 /// Ditto 248 nothrow pure @nogc 249 static Color brown() { return Color(128, 64, 0); } 250 251 nothrow pure @nogc 252 void premultiply() { 253 r = (r * a) / 255; 254 g = (g * a) / 255; 255 b = (b * a) / 255; 256 } 257 258 nothrow pure @nogc 259 void unPremultiply() { 260 r = cast(ubyte)(r * 255 / a); 261 g = cast(ubyte)(g * 255 / a); 262 b = cast(ubyte)(b * 255 / a); 263 } 264 265 266 /* 267 ubyte[4] toRgbaArray() { 268 return [r,g,b,a]; 269 } 270 */ 271 272 /// Return black-and-white color 273 Color toBW() () nothrow pure @safe @nogc { 274 // FIXME: gamma? 275 int intens = clampToByte(cast(int)(0.2126*r+0.7152*g+0.0722*b)); 276 return Color(intens, intens, intens, a); 277 } 278 279 /// Makes a string that matches CSS syntax for websites 280 string toCssString() const { 281 if(a == 255) 282 return "#" ~ toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b); 283 else { 284 return "rgba("~toInternal!string(r)~", "~toInternal!string(g)~", "~toInternal!string(b)~", "~toInternal!string(cast(double)a / 255.0)~")"; 285 } 286 } 287 288 /// Makes a hex string RRGGBBAA (aa only present if it is not 255) 289 string toString() const { 290 if(a == 255) 291 return toCssString()[1 .. $]; 292 else 293 return toRgbaHexString(); 294 } 295 296 /// returns RRGGBBAA, even if a== 255 297 string toRgbaHexString() const { 298 return toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b) ~ toHexInternal(a); 299 } 300 301 /// Gets a color by name, iff the name is one of the static members listed above 302 static Color fromNameString(string s) { 303 Color c; 304 foreach(member; __traits(allMembers, Color)) { 305 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 306 if(s == member) 307 return __traits(getMember, Color, member); 308 } 309 } 310 throw new Exception("Unknown color " ~ s); 311 } 312 313 /++ 314 Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa 315 316 History: 317 The short-form hex string parsing (`#fff`) was added on April 10, 2020. (v7.2.0) 318 +/ 319 static Color fromString(scope const(char)[] s) { 320 s = s.stripInternal(); 321 322 Color c; 323 c.a = 255; 324 325 // trying named colors via the static no-arg methods here 326 foreach(member; __traits(allMembers, Color)) { 327 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 328 if(s == member) 329 return __traits(getMember, Color, member); 330 } 331 } 332 333 // try various notations borrowed from CSS (though a little extended) 334 335 // hsl(h,s,l,a) where h is degrees and s,l,a are 0 >= x <= 1.0 336 if(s.startsWithInternal("hsl(") || s.startsWithInternal("hsla(")) { 337 assert(s[$-1] == ')'); 338 s = s[s.startsWithInternal("hsl(") ? 4 : 5 .. $ - 1]; // the closing paren 339 340 double[3] hsl; 341 ubyte a = 255; 342 343 s.splitInternal(',', (int i, scope const(char)[] part) { 344 if(i < 3) 345 hsl[i] = toInternal!double(part.stripInternal); 346 else 347 a = clampToByte(cast(int) (toInternal!double(part.stripInternal) * 255)); 348 }); 349 350 c = .fromHsl(hsl); 351 c.a = a; 352 353 return c; 354 } 355 356 // rgb(r,g,b,a) where r,g,b are 0-255 and a is 0-1.0 357 if(s.startsWithInternal("rgb(") || s.startsWithInternal("rgba(")) { 358 assert(s[$-1] == ')'); 359 s = s[s.startsWithInternal("rgb(") ? 4 : 5 .. $ - 1]; // the closing paren 360 361 s.splitInternal(',', (int i, scope const(char)[] part) { 362 // lol the loop-switch pattern 363 auto v = toInternal!double(part.stripInternal); 364 switch(i) { 365 case 0: // red 366 c.r = clampToByte(cast(int) v); 367 break; 368 case 1: 369 c.g = clampToByte(cast(int) v); 370 break; 371 case 2: 372 c.b = clampToByte(cast(int) v); 373 break; 374 case 3: 375 c.a = clampToByte(cast(int) (v * 255)); 376 break; 377 default: // ignore 378 } 379 }); 380 381 return c; 382 } 383 384 385 386 387 // otherwise let's try it as a hex string, really loosely 388 389 if(s.length && s[0] == '#') 390 s = s[1 .. $]; 391 392 // support short form #fff for example 393 if(s.length == 3 || s.length == 4) { 394 string n; 395 n.reserve(8); 396 foreach(ch; s) { 397 n ~= ch; 398 n ~= ch; 399 } 400 s = n; 401 } 402 403 // not a built in... do it as a hex string 404 if(s.length >= 2) { 405 c.r = fromHexInternal(s[0 .. 2]); 406 s = s[2 .. $]; 407 } 408 if(s.length >= 2) { 409 c.g = fromHexInternal(s[0 .. 2]); 410 s = s[2 .. $]; 411 } 412 if(s.length >= 2) { 413 c.b = fromHexInternal(s[0 .. 2]); 414 s = s[2 .. $]; 415 } 416 if(s.length >= 2) { 417 c.a = fromHexInternal(s[0 .. 2]); 418 s = s[2 .. $]; 419 } 420 421 return c; 422 } 423 424 /// from hsl 425 static Color fromHsl(double h, double s, double l) { 426 return .fromHsl(h, s, l); 427 } 428 429 // this is actually branch-less for ints on x86, and even for longs on x86_64 430 static ubyte clampToByte(T) (T n) pure nothrow @safe @nogc if (__traits(isIntegral, T)) { 431 static if (__VERSION__ > 2067) pragma(inline, true); 432 static if (T.sizeof == 2 || T.sizeof == 4) { 433 static if (__traits(isUnsigned, T)) { 434 return cast(ubyte)(n&0xff|(255-((-cast(int)(n < 256))>>24))); 435 } else { 436 n &= -cast(int)(n >= 0); 437 return cast(ubyte)(n|((255-cast(int)n)>>31)); 438 } 439 } else static if (T.sizeof == 1) { 440 static assert(__traits(isUnsigned, T), "clampToByte: signed byte? no, really?"); 441 return cast(ubyte)n; 442 } else static if (T.sizeof == 8) { 443 static if (__traits(isUnsigned, T)) { 444 return cast(ubyte)(n&0xff|(255-((-cast(long)(n < 256))>>56))); 445 } else { 446 n &= -cast(long)(n >= 0); 447 return cast(ubyte)(n|((255-cast(long)n)>>63)); 448 } 449 } else { 450 static assert(false, "clampToByte: integer too big"); 451 } 452 } 453 454 /** this mixin can be used to alphablend two `uint` colors; 455 * `colu32name` is variable that holds color to blend, 456 * `destu32name` is variable that holds "current" color (from surface, for example). 457 * alpha value of `destu32name` doesn't matter. 458 * alpha value of `colu32name` means: 255 for replace color, 0 for keep `destu32name`. 459 * 460 * WARNING! This function does blending in RGB space, and RGB space is not linear! 461 */ 462 public enum ColorBlendMixinStr(string colu32name, string destu32name) = "{ 463 immutable uint a_tmp_ = (256-(255-(("~colu32name~")>>24)))&(-(1-(((255-(("~colu32name~")>>24))+1)>>8))); // to not lose bits, but 255 should become 0 464 immutable uint dc_tmp_ = ("~destu32name~")&0xffffff; 465 immutable uint srb_tmp_ = (("~colu32name~")&0xff00ff); 466 immutable uint sg_tmp_ = (("~colu32name~")&0x00ff00); 467 immutable uint drb_tmp_ = (dc_tmp_&0xff00ff); 468 immutable uint dg_tmp_ = (dc_tmp_&0x00ff00); 469 immutable uint orb_tmp_ = (drb_tmp_+(((srb_tmp_-drb_tmp_)*a_tmp_+0x800080)>>8))&0xff00ff; 470 immutable uint og_tmp_ = (dg_tmp_+(((sg_tmp_-dg_tmp_)*a_tmp_+0x008000)>>8))&0x00ff00; 471 ("~destu32name~") = (orb_tmp_|og_tmp_)|0xff000000; /*&0xffffff;*/ 472 }"; 473 474 475 /// Perform alpha-blending of `fore` to this color, return new color. 476 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 477 Color alphaBlend (Color fore) const pure nothrow @trusted @nogc { 478 version(LittleEndian) { 479 static if (__VERSION__ > 2067) pragma(inline, true); 480 Color res; 481 res.asUint = asUint; 482 mixin(ColorBlendMixinStr!("fore.asUint", "res.asUint")); 483 return res; 484 } else { 485 alias foreground = fore; 486 alias background = this; 487 foreach(idx, ref part; foreground.components) 488 part = cast(ubyte) (part * foreground.a / 255 + background.components[idx] * (255 - foreground.a) / 255); 489 return foreground; 490 } 491 } 492 } 493 494 void premultiplyBgra(ubyte[] bgra) pure @nogc @safe nothrow in { assert(bgra.length == 4); } do { 495 auto a = bgra[3]; 496 497 bgra[2] = (bgra[2] * a) / 255; 498 bgra[1] = (bgra[1] * a) / 255; 499 bgra[0] = (bgra[0] * a) / 255; 500 } 501 502 void unPremultiplyRgba(ubyte[] rgba) pure @nogc @safe nothrow in { assert(rgba.length == 4); } do { 503 auto a = rgba[3]; 504 505 rgba[0] = cast(ubyte)(rgba[0] * 255 / a); 506 rgba[1] = cast(ubyte)(rgba[1] * 255 / a); 507 rgba[2] = cast(ubyte)(rgba[2] * 255 / a); 508 } 509 510 unittest { 511 Color c = Color.fromString("#fff"); 512 assert(c == Color.white); 513 assert(c == Color.fromString("#ffffff")); 514 515 c = Color.fromString("#f0f"); 516 assert(c == Color.fromString("rgb(255, 0, 255)")); 517 } 518 519 nothrow @safe 520 private string toHexInternal(ubyte b) { 521 string s; 522 if(b < 16) 523 s ~= '0'; 524 else { 525 ubyte t = (b & 0xf0) >> 4; 526 if(t >= 10) 527 s ~= 'A' + t - 10; 528 else 529 s ~= '0' + t; 530 b &= 0x0f; 531 } 532 if(b >= 10) 533 s ~= 'A' + b - 10; 534 else 535 s ~= '0' + b; 536 537 return s; 538 } 539 540 nothrow @safe @nogc pure 541 private ubyte fromHexInternal(in char[] s) { 542 int result = 0; 543 544 int exp = 1; 545 //foreach(c; retro(s)) { // FIXME: retro doesn't work right in dtojs 546 foreach_reverse(c; s) { 547 if(c >= 'A' && c <= 'F') 548 result += exp * (c - 'A' + 10); 549 else if(c >= 'a' && c <= 'f') 550 result += exp * (c - 'a' + 10); 551 else if(c >= '0' && c <= '9') 552 result += exp * (c - '0'); 553 else 554 // throw new Exception("invalid hex character: " ~ cast(char) c); 555 return 0; 556 557 exp *= 16; 558 } 559 560 return cast(ubyte) result; 561 } 562 563 /// Converts hsl to rgb 564 Color fromHsl(real[3] hsl) nothrow pure @safe @nogc { 565 return fromHsl(cast(double) hsl[0], cast(double) hsl[1], cast(double) hsl[2]); 566 } 567 568 Color fromHsl(double[3] hsl) nothrow pure @safe @nogc { 569 return fromHsl(hsl[0], hsl[1], hsl[2]); 570 } 571 572 /// Converts hsl to rgb 573 Color fromHsl(double h, double s, double l, double a = 255) nothrow pure @safe @nogc { 574 h = h % 360; 575 576 double C = (1 - absInternal(2 * l - 1)) * s; 577 578 double hPrime = h / 60; 579 580 double X = C * (1 - absInternal(hPrime % 2 - 1)); 581 582 double r, g, b; 583 584 if(h is double.nan) 585 r = g = b = 0; 586 else if (hPrime >= 0 && hPrime < 1) { 587 r = C; 588 g = X; 589 b = 0; 590 } else if (hPrime >= 1 && hPrime < 2) { 591 r = X; 592 g = C; 593 b = 0; 594 } else if (hPrime >= 2 && hPrime < 3) { 595 r = 0; 596 g = C; 597 b = X; 598 } else if (hPrime >= 3 && hPrime < 4) { 599 r = 0; 600 g = X; 601 b = C; 602 } else if (hPrime >= 4 && hPrime < 5) { 603 r = X; 604 g = 0; 605 b = C; 606 } else if (hPrime >= 5 && hPrime < 6) { 607 r = C; 608 g = 0; 609 b = X; 610 } 611 612 double m = l - C / 2; 613 614 r += m; 615 g += m; 616 b += m; 617 618 return Color( 619 cast(int)(r * 255), 620 cast(int)(g * 255), 621 cast(int)(b * 255), 622 cast(int)(a)); 623 } 624 625 /// Assumes the input `u` is already between 0 and 1 fyi. 626 nothrow pure @safe @nogc 627 double srgbToLinearRgb(double u) { 628 if(u < 0.4045) 629 return u / 12.92; 630 else 631 return ((u + 0.055) / 1.055) ^^ 2.4; 632 } 633 634 /// Converts an RGB color into an HSL triplet. useWeightedLightness will try to get a better value for luminosity for the human eye, which is more sensitive to green than red and more to red than blue. If it is false, it just does average of the rgb. 635 double[3] toHsl(Color c, bool useWeightedLightness = false) nothrow pure @trusted @nogc { 636 double r1 = cast(double) c.r / 255; 637 double g1 = cast(double) c.g / 255; 638 double b1 = cast(double) c.b / 255; 639 640 double maxColor = maxInternal(r1, g1, b1); 641 double minColor = minInternal(r1, g1, b1); 642 643 double L = (maxColor + minColor) / 2 ; 644 if(useWeightedLightness) { 645 // the colors don't affect the eye equally 646 // this is a little more accurate than plain HSL numbers 647 L = 0.2126*srgbToLinearRgb(r1) + 0.7152*srgbToLinearRgb(g1) + 0.0722*srgbToLinearRgb(b1); 648 // maybe a better number is 299, 587, 114 649 } 650 double S = 0; 651 double H = 0; 652 if(maxColor != minColor) { 653 if(L < 0.5) { 654 S = (maxColor - minColor) / (maxColor + minColor); 655 } else { 656 S = (maxColor - minColor) / (2.0 - maxColor - minColor); 657 } 658 if(r1 == maxColor) { 659 H = (g1-b1) / (maxColor - minColor); 660 } else if(g1 == maxColor) { 661 H = 2.0 + (b1 - r1) / (maxColor - minColor); 662 } else { 663 H = 4.0 + (r1 - g1) / (maxColor - minColor); 664 } 665 } 666 667 H = H * 60; 668 if(H < 0){ 669 H += 360; 670 } 671 672 return [H, S, L]; 673 } 674 675 /// . 676 Color lighten(Color c, double percentage) nothrow pure @safe @nogc { 677 auto hsl = toHsl(c); 678 hsl[2] *= (1 + percentage); 679 if(hsl[2] > 1) 680 hsl[2] = 1; 681 return fromHsl(hsl); 682 } 683 684 /// . 685 Color darken(Color c, double percentage) nothrow pure @safe @nogc { 686 auto hsl = toHsl(c); 687 hsl[2] *= (1 - percentage); 688 return fromHsl(hsl); 689 } 690 691 /// for light colors, call darken. for dark colors, call lighten. 692 /// The goal: get toward center grey. 693 Color moderate(Color c, double percentage) nothrow pure @safe @nogc { 694 auto hsl = toHsl(c); 695 if(hsl[2] > 0.5) 696 hsl[2] *= (1 - percentage); 697 else { 698 if(hsl[2] <= 0.01) // if we are given black, moderating it means getting *something* out 699 hsl[2] = percentage; 700 else 701 hsl[2] *= (1 + percentage); 702 } 703 if(hsl[2] > 1) 704 hsl[2] = 1; 705 return fromHsl(hsl); 706 } 707 708 /// the opposite of moderate. Make darks darker and lights lighter 709 Color extremify(Color c, double percentage) nothrow pure @safe @nogc { 710 auto hsl = toHsl(c, true); 711 if(hsl[2] < 0.5) 712 hsl[2] *= (1 - percentage); 713 else 714 hsl[2] *= (1 + percentage); 715 if(hsl[2] > 1) 716 hsl[2] = 1; 717 return fromHsl(hsl); 718 } 719 720 /// Move around the lightness wheel, trying not to break on moderate things 721 Color oppositeLightness(Color c) nothrow pure @safe @nogc { 722 auto hsl = toHsl(c); 723 724 auto original = hsl[2]; 725 726 if(original > 0.4 && original < 0.6) 727 hsl[2] = 0.8 - original; // so it isn't quite the same 728 else 729 hsl[2] = 1 - original; 730 731 return fromHsl(hsl); 732 } 733 734 /// Try to determine a text color - either white or black - based on the input 735 Color makeTextColor(Color c) nothrow pure @safe @nogc { 736 auto hsl = toHsl(c, true); // give green a bonus for contrast 737 if(hsl[2] > 0.71) 738 return Color(0, 0, 0); 739 else 740 return Color(255, 255, 255); 741 } 742 743 // These provide functional access to hsl manipulation; useful if you need a delegate 744 745 Color setLightness(Color c, double lightness) nothrow pure @safe @nogc { 746 auto hsl = toHsl(c); 747 hsl[2] = lightness; 748 return fromHsl(hsl); 749 } 750 751 752 /// 753 Color rotateHue(Color c, double degrees) nothrow pure @safe @nogc { 754 auto hsl = toHsl(c); 755 hsl[0] += degrees; 756 return fromHsl(hsl); 757 } 758 759 /// 760 Color setHue(Color c, double hue) nothrow pure @safe @nogc { 761 auto hsl = toHsl(c); 762 hsl[0] = hue; 763 return fromHsl(hsl); 764 } 765 766 /// 767 Color desaturate(Color c, double percentage) nothrow pure @safe @nogc { 768 auto hsl = toHsl(c); 769 hsl[1] *= (1 - percentage); 770 return fromHsl(hsl); 771 } 772 773 /// 774 Color saturate(Color c, double percentage) nothrow pure @safe @nogc { 775 auto hsl = toHsl(c); 776 hsl[1] *= (1 + percentage); 777 if(hsl[1] > 1) 778 hsl[1] = 1; 779 return fromHsl(hsl); 780 } 781 782 /// 783 Color setSaturation(Color c, double saturation) nothrow pure @safe @nogc { 784 auto hsl = toHsl(c); 785 hsl[1] = saturation; 786 return fromHsl(hsl); 787 } 788 789 790 /* 791 void main(string[] args) { 792 auto color1 = toHsl(Color(255, 0, 0)); 793 auto color = fromHsl(color1[0] + 60, color1[1], color1[2]); 794 795 writefln("#%02x%02x%02x", color.r, color.g, color.b); 796 } 797 */ 798 799 /* Color algebra functions */ 800 801 /* Alpha putpixel looks like this: 802 803 void putPixel(Image i, Color c) { 804 Color b; 805 b.r = i.data[(y * i.width + x) * bpp + 0]; 806 b.g = i.data[(y * i.width + x) * bpp + 1]; 807 b.b = i.data[(y * i.width + x) * bpp + 2]; 808 b.a = i.data[(y * i.width + x) * bpp + 3]; 809 810 float ca = cast(float) c.a / 255; 811 812 i.data[(y * i.width + x) * bpp + 0] = alpha(c.r, ca, b.r); 813 i.data[(y * i.width + x) * bpp + 1] = alpha(c.g, ca, b.g); 814 i.data[(y * i.width + x) * bpp + 2] = alpha(c.b, ca, b.b); 815 i.data[(y * i.width + x) * bpp + 3] = alpha(c.a, ca, b.a); 816 } 817 818 ubyte alpha(ubyte c1, float alpha, ubyte onto) { 819 auto got = (1 - alpha) * onto + alpha * c1; 820 821 if(got > 255) 822 return 255; 823 return cast(ubyte) got; 824 } 825 826 So, given the background color and the resultant color, what was 827 composited on to it? 828 */ 829 830 /// 831 ubyte unalpha(ubyte colorYouHave, float alpha, ubyte backgroundColor) nothrow pure @safe @nogc { 832 // resultingColor = (1-alpha) * backgroundColor + alpha * answer 833 auto resultingColorf = cast(float) colorYouHave; 834 auto backgroundColorf = cast(float) backgroundColor; 835 836 auto answer = (resultingColorf - backgroundColorf + alpha * backgroundColorf) / alpha; 837 return Color.clampToByte(cast(int) answer); 838 } 839 840 /// 841 ubyte makeAlpha(ubyte colorYouHave, ubyte backgroundColor/*, ubyte foreground = 0x00*/) nothrow pure @safe @nogc { 842 //auto foregroundf = cast(float) foreground; 843 auto foregroundf = 0.00f; 844 auto colorYouHavef = cast(float) colorYouHave; 845 auto backgroundColorf = cast(float) backgroundColor; 846 847 // colorYouHave = backgroundColorf - alpha * backgroundColorf + alpha * foregroundf 848 auto alphaf = 1 - colorYouHave / backgroundColorf; 849 alphaf *= 255; 850 851 return Color.clampToByte(cast(int) alphaf); 852 } 853 854 855 int fromHex(string s) { 856 int result = 0; 857 858 int exp = 1; 859 // foreach(c; retro(s)) { 860 foreach_reverse(c; s) { 861 if(c >= 'A' && c <= 'F') 862 result += exp * (c - 'A' + 10); 863 else if(c >= 'a' && c <= 'f') 864 result += exp * (c - 'a' + 10); 865 else if(c >= '0' && c <= '9') 866 result += exp * (c - '0'); 867 else 868 throw new Exception("invalid hex character: " ~ cast(char) c); 869 870 exp *= 16; 871 } 872 873 return result; 874 } 875 876 /// 877 Color colorFromString(string s) { 878 if(s.length == 0) 879 return Color(0,0,0,255); 880 if(s[0] == '#') 881 s = s[1..$]; 882 assert(s.length == 6 || s.length == 8); 883 884 Color c; 885 886 c.r = cast(ubyte) fromHex(s[0..2]); 887 c.g = cast(ubyte) fromHex(s[2..4]); 888 c.b = cast(ubyte) fromHex(s[4..6]); 889 if(s.length == 8) 890 c.a = cast(ubyte) fromHex(s[6..8]); 891 else 892 c.a = 255; 893 894 return c; 895 } 896 897 /* 898 import browser.window; 899 import std.conv; 900 void main() { 901 import browser.document; 902 foreach(ele; document.querySelectorAll("input")) { 903 ele.addEventListener("change", { 904 auto h = toInternal!double(document.querySelector("input[name=h]").value); 905 auto s = toInternal!double(document.querySelector("input[name=s]").value); 906 auto l = toInternal!double(document.querySelector("input[name=l]").value); 907 908 Color c = Color.fromHsl(h, s, l); 909 910 auto e = document.getElementById("example"); 911 e.style.backgroundColor = c.toCssString(); 912 913 // JSElement __js_this; 914 // __js_this.style.backgroundColor = c.toCssString(); 915 }, false); 916 } 917 } 918 */ 919 920 921 922 /** 923 This provides two image classes and a bunch of functions that work on them. 924 925 Why are they separate classes? I think the operations on the two of them 926 are necessarily different. There's a whole bunch of operations that only 927 really work on truecolor (blurs, gradients), and a few that only work 928 on indexed images (palette swaps). 929 930 Even putpixel is pretty different. On indexed, it is a palette entry's 931 index number. On truecolor, it is the actual color. 932 933 A greyscale image is the weird thing in the middle. It is truecolor, but 934 fits in the same size as indexed. Still, I'd say it is a specialization 935 of truecolor. 936 937 There is a subset that works on both 938 939 */ 940 941 /// An image in memory 942 interface MemoryImage { 943 //IndexedImage convertToIndexedImage() const; 944 //TrueColorImage convertToTrueColor() const; 945 946 /// gets it as a TrueColorImage. May return this or may do a conversion and return a new image 947 TrueColorImage getAsTrueColorImage() pure nothrow @safe; 948 949 /// Image width, in pixels 950 int width() const pure nothrow @safe @nogc; 951 952 /// Image height, in pixels 953 int height() const pure nothrow @safe @nogc; 954 955 /// Get image pixel. Slow, but returns valid RGBA color (completely transparent for off-image pixels). 956 Color getPixel(int x, int y) const pure nothrow @safe @nogc; 957 958 /// Set image pixel. 959 void setPixel(int x, int y, in Color clr) nothrow @safe; 960 961 /// Returns a copy of the image 962 MemoryImage clone() const pure nothrow @safe; 963 964 /// Load image from file. This will import arsd.image to do the actual work, and cost nothing if you don't use it. 965 static MemoryImage fromImage(T : const(char)[]) (T filename) @trusted { 966 static if (__traits(compiles, (){import arsd.image;})) { 967 // yay, we have image loader here, try it! 968 import arsd.image; 969 return loadImageFromFile(filename); 970 } else { 971 static assert(0, "please provide 'arsd.image' to load images!"); 972 } 973 } 974 975 // ***This method is deliberately not publicly documented.*** 976 // What it does is unconditionally frees internal image storage, without any sanity checks. 977 // If you will do this, make sure that you have no references to image data left (like 978 // slices of [data] array, for example). Those references will become invalid, and WILL 979 // lead to Undefined Behavior. 980 // tl;dr: IF YOU HAVE *ANY* QUESTIONS REGARDING THIS COMMENT, DON'T USE THIS! 981 // Note to implementors: it is safe to simply do nothing in this method. 982 // Also, it should be safe to call this method twice or more. 983 void clearInternal () nothrow @system;// @nogc; // nogc is commented right now just because GC.free is only @nogc in newest dmd and i want to stay compatible a few versions back too. it can be added later 984 985 /// Convenient alias for `fromImage` 986 alias fromImageFile = fromImage; 987 } 988 989 /// An image that consists of indexes into a color palette. Use [getAsTrueColorImage]() if you don't care about palettes 990 class IndexedImage : MemoryImage { 991 bool hasAlpha; 992 993 /// . 994 Color[] palette; 995 /// the data as indexes into the palette. Stored left to right, top to bottom, no padding. 996 ubyte[] data; 997 998 override void clearInternal () nothrow @system {// @nogc { 999 import core.memory : GC; 1000 // it is safe to call [GC.free] with `null` pointer. 1001 GC.free(GC.addrOf(palette.ptr)); palette = null; 1002 GC.free(GC.addrOf(data.ptr)); data = null; 1003 _width = _height = 0; 1004 } 1005 1006 /// . 1007 override int width() const pure nothrow @safe @nogc { 1008 return _width; 1009 } 1010 1011 /// . 1012 override int height() const pure nothrow @safe @nogc { 1013 return _height; 1014 } 1015 1016 /// . 1017 override IndexedImage clone() const pure nothrow @trusted { 1018 auto n = new IndexedImage(width, height); 1019 n.data[] = this.data[]; // the data member is already there, so array copy 1020 n.palette = this.palette.dup; // and here we need to allocate too, so dup 1021 n.hasAlpha = this.hasAlpha; 1022 return n; 1023 } 1024 1025 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1026 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1027 size_t pos = cast(size_t)y*_width+x; 1028 if (pos >= data.length) return Color(0, 0, 0, 0); 1029 ubyte b = data.ptr[pos]; 1030 if (b >= palette.length) return Color(0, 0, 0, 0); 1031 return palette.ptr[b]; 1032 } else { 1033 return Color(0, 0, 0, 0); 1034 } 1035 } 1036 1037 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1038 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1039 size_t pos = cast(size_t)y*_width+x; 1040 if (pos >= data.length) return; 1041 ubyte pidx = findNearestColor(palette, clr); 1042 if (palette.length < 255 && 1043 (palette.ptr[pidx].r != clr.r || palette.ptr[pidx].g != clr.g || palette.ptr[pidx].b != clr.b || palette.ptr[pidx].a != clr.a)) { 1044 // add new color 1045 pidx = addColor(clr); 1046 } 1047 data.ptr[pos] = pidx; 1048 } 1049 } 1050 1051 private int _width; 1052 private int _height; 1053 1054 /// . 1055 this(int w, int h) pure nothrow @safe { 1056 _width = w; 1057 _height = h; 1058 1059 // ensure that the computed size does not exceed basic address space limits 1060 assert(cast(ulong)w * h <= size_t.max); 1061 // upcast to avoid overflow for images larger than 536 Mpix 1062 data = new ubyte[cast(size_t)w*h]; 1063 } 1064 1065 /* 1066 void resize(int w, int h, bool scale) { 1067 1068 } 1069 */ 1070 1071 /// returns a new image 1072 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1073 return convertToTrueColor(); 1074 } 1075 1076 /// Creates a new TrueColorImage based on this data 1077 TrueColorImage convertToTrueColor() const pure nothrow @trusted { 1078 auto tci = new TrueColorImage(width, height); 1079 foreach(i, b; data) { 1080 tci.imageData.colors[i] = palette[b]; 1081 } 1082 return tci; 1083 } 1084 1085 /// Gets an exact match, if possible, adds if not. See also: the findNearestColor free function. 1086 ubyte getOrAddColor(Color c) nothrow @trusted { 1087 foreach(i, co; palette) { 1088 if(c == co) 1089 return cast(ubyte) i; 1090 } 1091 1092 return addColor(c); 1093 } 1094 1095 /// Number of colors currently in the palette (note: palette entries are not necessarily used in the image data) 1096 int numColors() const pure nothrow @trusted @nogc { 1097 return cast(int) palette.length; 1098 } 1099 1100 /// Adds an entry to the palette, returning its index 1101 ubyte addColor(Color c) nothrow @trusted { 1102 assert(palette.length < 256); 1103 if(c.a != 255) 1104 hasAlpha = true; 1105 palette ~= c; 1106 1107 return cast(ubyte) (palette.length - 1); 1108 } 1109 } 1110 1111 /// An RGBA array of image data. Use the free function quantize() to convert to an IndexedImage 1112 class TrueColorImage : MemoryImage { 1113 // bool hasAlpha; 1114 // bool isGreyscale; 1115 1116 //ubyte[] data; // stored as rgba quads, upper left to right to bottom 1117 /// . 1118 struct Data { 1119 ubyte[] bytes; /// the data as rgba bytes. Stored left to right, top to bottom, no padding. 1120 // the union is no good because the length of the struct is wrong! 1121 1122 /// the same data as Color structs 1123 @trusted // the cast here is typically unsafe, but it is ok 1124 // here because I guarantee the layout, note the static assert below 1125 @property inout(Color)[] colors() inout pure nothrow @nogc { 1126 return cast(inout(Color)[]) bytes; 1127 } 1128 1129 static assert(Color.sizeof == 4); 1130 } 1131 1132 /// . 1133 Data imageData; 1134 alias imageData.bytes data; 1135 1136 int _width; 1137 int _height; 1138 1139 override void clearInternal () nothrow @system {// @nogc { 1140 import core.memory : GC; 1141 // it is safe to call [GC.free] with `null` pointer. 1142 GC.free(GC.addrOf(imageData.bytes.ptr)); imageData.bytes = null; 1143 _width = _height = 0; 1144 } 1145 1146 /// . 1147 override TrueColorImage clone() const pure nothrow @trusted { 1148 auto n = new TrueColorImage(width, height); 1149 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1150 return n; 1151 } 1152 1153 /// . 1154 override int width() const pure nothrow @trusted @nogc { return _width; } 1155 ///. 1156 override int height() const pure nothrow @trusted @nogc { return _height; } 1157 1158 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1159 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1160 size_t pos = cast(size_t)y*_width+x; 1161 return imageData.colors.ptr[pos]; 1162 } else { 1163 return Color(0, 0, 0, 0); 1164 } 1165 } 1166 1167 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1168 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1169 size_t pos = cast(size_t)y*_width+x; 1170 if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1171 } 1172 } 1173 1174 /// . 1175 this(int w, int h) pure nothrow @safe { 1176 _width = w; 1177 _height = h; 1178 1179 // ensure that the computed size does not exceed basic address space limits 1180 assert(cast(ulong)w * h * 4 <= size_t.max); 1181 // upcast to avoid overflow for images larger than 536 Mpix 1182 imageData.bytes = new ubyte[cast(size_t)w * h * 4]; 1183 } 1184 1185 /// Creates with existing data. The data pointer is stored here. 1186 this(int w, int h, ubyte[] data) pure nothrow @safe { 1187 _width = w; 1188 _height = h; 1189 assert(cast(ulong)w * h * 4 <= size_t.max); 1190 assert(data.length == cast(size_t)w * h * 4); 1191 imageData.bytes = data; 1192 } 1193 1194 /// Returns this 1195 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1196 return this; 1197 } 1198 } 1199 1200 /+ 1201 /// An RGB array of image data. 1202 class TrueColorImageWithoutAlpha : MemoryImage { 1203 struct Data { 1204 ubyte[] bytes; // the data as rgba bytes. Stored left to right, top to bottom, no padding. 1205 } 1206 1207 /// . 1208 Data imageData; 1209 1210 int _width; 1211 int _height; 1212 1213 override void clearInternal () nothrow @system {// @nogc { 1214 import core.memory : GC; 1215 // it is safe to call [GC.free] with `null` pointer. 1216 GC.free(imageData.bytes.ptr); imageData.bytes = null; 1217 _width = _height = 0; 1218 } 1219 1220 /// . 1221 override TrueColorImageWithoutAlpha clone() const pure nothrow @trusted { 1222 auto n = new TrueColorImageWithoutAlpha(width, height); 1223 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1224 return n; 1225 } 1226 1227 /// . 1228 override int width() const pure nothrow @trusted @nogc { return _width; } 1229 ///. 1230 override int height() const pure nothrow @trusted @nogc { return _height; } 1231 1232 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1233 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1234 uint pos = (y*_width+x) * 3; 1235 return Color(imageData.bytes[0], imageData.bytes[1], imageData.bytes[2], 255); 1236 } else { 1237 return Color(0, 0, 0, 0); 1238 } 1239 } 1240 1241 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1242 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1243 uint pos = y*_width+x; 1244 //if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1245 // FIXME 1246 } 1247 } 1248 1249 /// . 1250 this(int w, int h) pure nothrow @safe { 1251 _width = w; 1252 _height = h; 1253 imageData.bytes = new ubyte[w*h*3]; 1254 } 1255 1256 /// Creates with existing data. The data pointer is stored here. 1257 this(int w, int h, ubyte[] data) pure nothrow @safe { 1258 _width = w; 1259 _height = h; 1260 assert(data.length == w * h * 3); 1261 imageData.bytes = data; 1262 } 1263 1264 /// 1265 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1266 // FIXME 1267 //return this; 1268 } 1269 } 1270 +/ 1271 1272 1273 alias extern(C) int function(scope const void*, scope const void*) @system Comparator; 1274 @trusted void nonPhobosSort(T)(T[] obj, Comparator comparator) { 1275 import core.stdc.stdlib; 1276 qsort(obj.ptr, obj.length, typeof(obj[0]).sizeof, comparator); 1277 } 1278 1279 /// Converts true color to an indexed image. It uses palette as the starting point, adding entries 1280 /// until maxColors as needed. If palette is null, it creates a whole new palette. 1281 /// 1282 /// After quantizing the image, it applies a dithering algorithm. 1283 /// 1284 /// This is not written for speed. 1285 IndexedImage quantize(in TrueColorImage img, Color[] palette = null, in int maxColors = 256) 1286 // this is just because IndexedImage assumes ubyte palette values 1287 in { assert(maxColors <= 256); } 1288 do { 1289 int[Color] uses; 1290 foreach(pixel; img.imageData.colors) { 1291 if(auto i = pixel in uses) { 1292 (*i)++; 1293 } else { 1294 uses[pixel] = 1; 1295 } 1296 } 1297 1298 struct ColorUse { 1299 Color c; 1300 int uses; 1301 //string toString() { import std.conv; return c.toCssString() ~ " x " ~ to!string(uses); } 1302 int opCmp(ref const ColorUse co) const { 1303 return co.uses - uses; 1304 } 1305 extern(C) static int comparator(scope const void* lhs, scope const void* rhs) { 1306 return (cast(ColorUse*)rhs).uses - (cast(ColorUse*)lhs).uses; 1307 } 1308 } 1309 1310 ColorUse[] sorted; 1311 1312 foreach(color, count; uses) 1313 sorted ~= ColorUse(color, count); 1314 1315 uses = null; 1316 1317 nonPhobosSort(sorted, &ColorUse.comparator); 1318 // or, with phobos, but that adds 70ms to compile time 1319 //import std.algorithm.sorting : sort; 1320 //sort(sorted); 1321 1322 ubyte[Color] paletteAssignments; 1323 foreach(idx, entry; palette) 1324 paletteAssignments[entry] = cast(ubyte) idx; 1325 1326 // For the color assignments from the image, I do multiple passes, decreasing the acceptable 1327 // distance each time until we're full. 1328 1329 // This is probably really slow.... but meh it gives pretty good results. 1330 1331 auto ddiff = 32; 1332 outer: for(int d1 = 128; d1 >= 0; d1 -= ddiff) { 1333 auto minDist = d1*d1; 1334 if(d1 <= 64) 1335 ddiff = 16; 1336 if(d1 <= 32) 1337 ddiff = 8; 1338 foreach(possibility; sorted) { 1339 if(palette.length == maxColors) 1340 break; 1341 if(palette.length) { 1342 auto co = palette[findNearestColor(palette, possibility.c)]; 1343 auto pixel = possibility.c; 1344 1345 auto dr = cast(int) co.r - pixel.r; 1346 auto dg = cast(int) co.g - pixel.g; 1347 auto db = cast(int) co.b - pixel.b; 1348 1349 auto dist = dr*dr + dg*dg + db*db; 1350 // not good enough variety to justify an allocation yet 1351 if(dist < minDist) 1352 continue; 1353 } 1354 paletteAssignments[possibility.c] = cast(ubyte) palette.length; 1355 palette ~= possibility.c; 1356 } 1357 } 1358 1359 // Final pass: just fill in any remaining space with the leftover common colors 1360 while(palette.length < maxColors && sorted.length) { 1361 if(sorted[0].c !in paletteAssignments) { 1362 paletteAssignments[sorted[0].c] = cast(ubyte) palette.length; 1363 palette ~= sorted[0].c; 1364 } 1365 sorted = sorted[1 .. $]; 1366 } 1367 1368 1369 bool wasPerfect = true; 1370 auto newImage = new IndexedImage(img.width, img.height); 1371 newImage.palette = palette; 1372 foreach(idx, pixel; img.imageData.colors) { 1373 if(auto p = pixel in paletteAssignments) 1374 newImage.data[idx] = *p; 1375 else { 1376 // gotta find the closest one... 1377 newImage.data[idx] = findNearestColor(palette, pixel); 1378 wasPerfect = false; 1379 } 1380 } 1381 1382 if(!wasPerfect) 1383 floydSteinbergDither(newImage, img); 1384 1385 return newImage; 1386 } 1387 1388 /// Finds the best match for pixel in palette (currently by checking for minimum euclidean distance in rgb colorspace) 1389 ubyte findNearestColor(in Color[] palette, in Color pixel) nothrow pure @trusted @nogc { 1390 int best = 0; 1391 int bestDistance = int.max; 1392 foreach(pe, co; palette) { 1393 auto dr = cast(int) co.r - pixel.r; 1394 auto dg = cast(int) co.g - pixel.g; 1395 auto db = cast(int) co.b - pixel.b; 1396 int dist = dr*dr + dg*dg + db*db; 1397 1398 if(dist < bestDistance) { 1399 best = cast(int) pe; 1400 bestDistance = dist; 1401 } 1402 } 1403 1404 return cast(ubyte) best; 1405 } 1406 1407 /+ 1408 1409 // Quantizing and dithering test program 1410 1411 void main( ){ 1412 /* 1413 auto img = new TrueColorImage(256, 32); 1414 foreach(y; 0 .. img.height) { 1415 foreach(x; 0 .. img.width) { 1416 img.imageData.colors[x + y * img.width] = Color(x, y * (255 / img.height), 0); 1417 } 1418 } 1419 */ 1420 1421 TrueColorImage img; 1422 1423 { 1424 1425 import arsd.png; 1426 1427 struct P { 1428 ubyte[] range; 1429 void put(ubyte[] a) { range ~= a; } 1430 } 1431 1432 P range; 1433 import std.algorithm; 1434 1435 import std.stdio; 1436 writePngLazy(range, pngFromBytes(File("/home/me/nyesha.png").byChunk(4096)).byRgbaScanline.map!((line) { 1437 foreach(ref pixel; line.pixels) { 1438 continue; 1439 auto sum = cast(int) pixel.r + pixel.g + pixel.b; 1440 ubyte a = cast(ubyte)(sum / 3); 1441 pixel.r = a; 1442 pixel.g = a; 1443 pixel.b = a; 1444 } 1445 return line; 1446 })); 1447 1448 img = imageFromPng(readPng(range.range)).getAsTrueColorImage; 1449 1450 1451 } 1452 1453 1454 1455 auto qimg = quantize(img, null, 2); 1456 1457 import arsd.simpledisplay; 1458 auto win = new SimpleWindow(img.width, img.height * 3); 1459 auto painter = win.draw(); 1460 painter.drawImage(Point(0, 0), Image.fromMemoryImage(img)); 1461 painter.drawImage(Point(0, img.height), Image.fromMemoryImage(qimg)); 1462 floydSteinbergDither(qimg, img); 1463 painter.drawImage(Point(0, img.height * 2), Image.fromMemoryImage(qimg)); 1464 win.eventLoop(0); 1465 } 1466 +/ 1467 1468 /+ 1469 /// If the background is transparent, it simply erases the alpha channel. 1470 void removeTransparency(IndexedImage img, Color background) 1471 +/ 1472 1473 /// Perform alpha-blending of `fore` to this color, return new color. 1474 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 1475 Color alphaBlend(Color foreground, Color background) pure nothrow @safe @nogc { 1476 //if(foreground.a == 255) 1477 //return foreground; 1478 if(foreground.a == 0) 1479 return background; // the other blend function always returns alpha 255, but if the foreground has nothing, we should keep the background the same so its antialiasing doesn't get smashed (assuming this is blending in like a png instead of on a framebuffer) 1480 1481 static if (__VERSION__ > 2067) pragma(inline, true); 1482 return background.alphaBlend(foreground); 1483 } 1484 1485 /* 1486 /// Reduces the number of colors in a palette. 1487 void reducePaletteSize(IndexedImage img, int maxColors = 16) { 1488 1489 } 1490 */ 1491 1492 // I think I did this wrong... but the results aren't too bad so the bug can't be awful. 1493 /// Dithers img in place to look more like original. 1494 void floydSteinbergDither(IndexedImage img, in TrueColorImage original) nothrow @trusted { 1495 assert(img.width == original.width); 1496 assert(img.height == original.height); 1497 1498 auto buffer = new Color[](original.imageData.colors.length); 1499 1500 int x, y; 1501 1502 foreach(idx, c; original.imageData.colors) { 1503 auto n = img.palette[img.data[idx]]; 1504 int errorR = cast(int) c.r - n.r; 1505 int errorG = cast(int) c.g - n.g; 1506 int errorB = cast(int) c.b - n.b; 1507 1508 void doit(int idxOffset, int multiplier) { 1509 // if(idx + idxOffset < buffer.length) 1510 buffer[idx + idxOffset] = Color.fromIntegers( 1511 c.r + multiplier * errorR / 16, 1512 c.g + multiplier * errorG / 16, 1513 c.b + multiplier * errorB / 16, 1514 c.a 1515 ); 1516 } 1517 1518 if((x+1) != original.width) 1519 doit(1, 7); 1520 if((y+1) != original.height) { 1521 if(x != 0) 1522 doit(-1 + img.width, 3); 1523 doit(img.width, 5); 1524 if(x+1 != original.width) 1525 doit(1 + img.width, 1); 1526 } 1527 1528 img.data[idx] = findNearestColor(img.palette, buffer[idx]); 1529 1530 x++; 1531 if(x == original.width) { 1532 x = 0; 1533 y++; 1534 } 1535 } 1536 } 1537 1538 // these are just really useful in a lot of places where the color/image functions are used, 1539 // so I want them available with Color 1540 /// 1541 struct Point { 1542 int x; /// 1543 int y; /// 1544 1545 pure const nothrow @safe: 1546 1547 Point opBinary(string op)(in Point rhs) @nogc { 1548 return Point(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); 1549 } 1550 1551 Point opBinary(string op)(int rhs) @nogc { 1552 return Point(mixin("x" ~ op ~ "rhs"), mixin("y" ~ op ~ "rhs")); 1553 } 1554 } 1555 1556 /// 1557 struct Size { 1558 int width; /// 1559 int height; /// 1560 1561 int area() pure nothrow @safe const @nogc { return width * height; } 1562 } 1563 1564 /// 1565 struct Rectangle { 1566 int left; /// 1567 int top; /// 1568 int right; /// 1569 int bottom; /// 1570 1571 pure const nothrow @safe @nogc: 1572 1573 /// 1574 this(int left, int top, int right, int bottom) { 1575 this.left = left; 1576 this.top = top; 1577 this.right = right; 1578 this.bottom = bottom; 1579 } 1580 1581 /// 1582 this(in Point upperLeft, in Point lowerRight) { 1583 this(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y); 1584 } 1585 1586 /// 1587 this(in Point upperLeft, in Size size) { 1588 this(upperLeft.x, upperLeft.y, upperLeft.x + size.width, upperLeft.y + size.height); 1589 } 1590 1591 /// 1592 @property Point upperLeft() { 1593 return Point(left, top); 1594 } 1595 1596 /// 1597 @property Point upperRight() { 1598 return Point(right, top); 1599 } 1600 1601 /// 1602 @property Point lowerLeft() { 1603 return Point(left, bottom); 1604 } 1605 1606 /// 1607 @property Point lowerRight() { 1608 return Point(right, bottom); 1609 } 1610 1611 /// 1612 @property Point center() { 1613 return Point((right + left) / 2, (bottom + top) / 2); 1614 } 1615 1616 /// 1617 @property Size size() { 1618 return Size(width, height); 1619 } 1620 1621 /// 1622 @property int width() { 1623 return right - left; 1624 } 1625 1626 /// 1627 @property int height() { 1628 return bottom - top; 1629 } 1630 1631 /// Returns true if this rectangle entirely contains the other 1632 bool contains(in Rectangle r) { 1633 return contains(r.upperLeft) && contains(r.lowerRight); 1634 } 1635 1636 /// ditto 1637 bool contains(in Point p) { 1638 return (p.x >= left && p.x < right && p.y >= top && p.y < bottom); 1639 } 1640 1641 /// Returns true of the two rectangles at any point overlap 1642 bool overlaps(in Rectangle r) { 1643 // the -1 in here are because right and top are exclusive 1644 return !((right-1) < r.left || (r.right-1) < left || (bottom-1) < r.top || (r.bottom-1) < top); 1645 } 1646 1647 /++ 1648 Returns a Rectangle representing the intersection of this and the other given one. 1649 1650 History: 1651 Added July 1, 2021 1652 +/ 1653 Rectangle intersectionOf(in Rectangle r) { 1654 auto tmp = Rectangle(max(left, r.left), max(top, r.top), min(right, r.right), min(bottom, r.bottom)); 1655 if(tmp.left >= tmp.right || tmp.top >= tmp.bottom) 1656 tmp = Rectangle.init; 1657 1658 return tmp; 1659 } 1660 } 1661 1662 private int max(int a, int b) @nogc nothrow pure @safe { 1663 return a >= b ? a : b; 1664 } 1665 private int min(int a, int b) @nogc nothrow pure @safe { 1666 return a <= b ? a : b; 1667 } 1668 1669 /++ 1670 Implements a flood fill algorithm, like the bucket tool in 1671 MS Paint. 1672 1673 Note it assumes `what.length == width*height`. 1674 1675 Params: 1676 what = the canvas to work with, arranged as top to bottom, left to right elements 1677 width = the width of the canvas 1678 height = the height of the canvas 1679 target = the type to replace. You may pass the existing value if you want to do what Paint does 1680 replacement = the replacement value 1681 x = the x-coordinate to start the fill (think of where the user clicked in Paint) 1682 y = the y-coordinate to start the fill 1683 additionalCheck = A custom additional check to perform on each square before continuing. Returning true means keep flooding, returning false means stop. If null, it is not used. 1684 +/ 1685 void floodFill(T)( 1686 T[] what, int width, int height, // the canvas to inspect 1687 T target, T replacement, // fill params 1688 int x, int y, bool delegate(int x, int y) @safe additionalCheck) // the node 1689 1690 // in(what.length == width * height) // gdc doesn't support this syntax yet so not gonna use it until that comes out. 1691 { 1692 assert(what.length == width * height); // will use the contract above when gdc supports it 1693 1694 T node = what[y * width + x]; 1695 1696 if(target == replacement) return; 1697 1698 if(node != target) return; 1699 1700 if(additionalCheck is null) 1701 additionalCheck = (int, int) => true; 1702 1703 if(!additionalCheck(x, y)) 1704 return; 1705 1706 Point[] queue; 1707 1708 queue ~= Point(x, y); 1709 1710 while(queue.length) { 1711 auto n = queue[0]; 1712 queue = queue[1 .. $]; 1713 //queue.assumeSafeAppend(); // lol @safe breakage 1714 1715 auto w = n; 1716 int offset = cast(int) (n.y * width + n.x); 1717 auto e = n; 1718 auto eoffset = offset; 1719 w.x--; 1720 offset--; 1721 while(w.x >= 0 && what[offset] == target && additionalCheck(w.x, w.y)) { 1722 w.x--; 1723 offset--; 1724 } 1725 while(e.x < width && what[eoffset] == target && additionalCheck(e.x, e.y)) { 1726 e.x++; 1727 eoffset++; 1728 } 1729 1730 // to make it inclusive again 1731 w.x++; 1732 offset++; 1733 foreach(o ; offset .. eoffset) { 1734 what[o] = replacement; 1735 if(w.y && what[o - width] == target && additionalCheck(w.x, w.y)) 1736 queue ~= Point(w.x, w.y - 1); 1737 if(w.y + 1 < height && what[o + width] == target && additionalCheck(w.x, w.y)) 1738 queue ~= Point(w.x, w.y + 1); 1739 w.x++; 1740 } 1741 } 1742 1743 /+ 1744 what[y * width + x] = replacement; 1745 1746 if(x) 1747 floodFill(what, width, height, target, replacement, 1748 x - 1, y, additionalCheck); 1749 1750 if(x != width - 1) 1751 floodFill(what, width, height, target, replacement, 1752 x + 1, y, additionalCheck); 1753 1754 if(y) 1755 floodFill(what, width, height, target, replacement, 1756 x, y - 1, additionalCheck); 1757 1758 if(y != height - 1) 1759 floodFill(what, width, height, target, replacement, 1760 x, y + 1, additionalCheck); 1761 +/ 1762 } 1763 1764 // for scripting, so you can tag it without strictly needing to import arsd.jsvar 1765 enum arsd_jsvar_compatible = "arsd_jsvar_compatible";