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