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