1 module arsd.color; 2 3 @safe: 4 5 // importing phobos explodes the size of this code 10x, so not doing it. 6 7 private { 8 real toInternal(T)(string s) { 9 real accumulator = 0.0; 10 size_t i = s.length; 11 foreach(idx, c; s) { 12 if(c >= '0' && c <= '9') { 13 accumulator *= 10; 14 accumulator += c - '0'; 15 } else if(c == '.') { 16 i = idx + 1; 17 break; 18 } else 19 throw new Exception("bad char to make real from " ~ s); 20 } 21 22 real accumulator2 = 0.0; 23 real count = 1; 24 foreach(c; s[i .. $]) { 25 if(c >= '0' && c <= '9') { 26 accumulator2 *= 10; 27 accumulator2 += c - '0'; 28 count *= 10; 29 } else 30 throw new Exception("bad char to make real from " ~ s); 31 } 32 33 return accumulator + accumulator2 / count; 34 } 35 36 @trusted 37 string toInternal(T)(int a) { 38 if(a == 0) 39 return "0"; 40 char[] ret; 41 while(a) { 42 ret ~= (a % 10) + '0'; 43 a /= 10; 44 } 45 for(int i = 0; i < ret.length / 2; i++) { 46 char c = ret[i]; 47 ret[i] = ret[$ - i - 1]; 48 ret[$ - i - 1] = c; 49 } 50 return cast(string) ret; 51 } 52 string toInternal(T)(real a) { 53 // a simplifying assumption here is the fact that we only use this in one place: toInternal!string(cast(real) a / 255) 54 // thus we know this will always be between 0.0 and 1.0, inclusive. 55 if(a <= 0.0) 56 return "0.0"; 57 if(a >= 1.0) 58 return "1.0"; 59 string ret = "0."; 60 // I wonder if I can handle round off error any better. Phobos does, but that isn't worth 100 KB of code. 61 int amt = cast(int)(a * 1000); 62 return ret ~ toInternal!string(amt); 63 } 64 65 real absInternal(real a) { return a < 0 ? -a : a; } 66 real minInternal(real a, real b, real c) { 67 auto m = a; 68 if(b < m) m = b; 69 if(c < m) m = c; 70 return m; 71 } 72 real maxInternal(real a, real b, real c) { 73 auto m = a; 74 if(b > m) m = b; 75 if(c > m) m = c; 76 return m; 77 } 78 bool startsWithInternal(string a, string b) { 79 return (a.length >= b.length && a[0 .. b.length] == b); 80 } 81 string[] splitInternal(string a, char c) { 82 string[] ret; 83 size_t previous = 0; 84 foreach(i, char ch; a) { 85 if(ch == c) { 86 ret ~= a[previous .. i]; 87 previous = i + 1; 88 } 89 } 90 if(previous != a.length) 91 ret ~= a[previous .. $]; 92 return ret; 93 } 94 string stripInternal(string s) { 95 foreach(i, char c; s) 96 if(c != ' ' && c != '\t' && c != '\n') { 97 s = s[i .. $]; 98 break; 99 } 100 for(int a = cast(int)(s.length - 1); a > 0; a--) { 101 char c = s[a]; 102 if(c != ' ' && c != '\t' && c != '\n') { 103 s = s[0 .. a + 1]; 104 break; 105 } 106 } 107 108 return s; 109 } 110 } 111 112 // done with mini-phobos 113 114 /// Represents an RGBA color 115 struct Color { 116 union { 117 ubyte[4] components; 118 119 struct { 120 ubyte r; /// red 121 ubyte g; /// green 122 ubyte b; /// blue 123 ubyte a; /// alpha. 255 == opaque 124 } 125 126 uint asUint; 127 } 128 129 // this makes sure they are in range before casting 130 static Color fromIntegers(int red, int green, int blue, int alpha = 255) { 131 if(red < 0) red = 0; if(red > 255) red = 255; 132 if(green < 0) green = 0; if(green > 255) green = 255; 133 if(blue < 0) blue = 0; if(blue > 255) blue = 255; 134 if(alpha < 0) alpha = 0; if(alpha > 255) alpha = 255; 135 return Color(red, green, blue, alpha); 136 } 137 138 /// . 139 this(int red, int green, int blue, int alpha = 255) { 140 // workaround dmd bug 10937 141 if(__ctfe) 142 this.components[0] = cast(ubyte) red; 143 else 144 this.r = cast(ubyte) red; 145 this.g = cast(ubyte) green; 146 this.b = cast(ubyte) blue; 147 this.a = cast(ubyte) alpha; 148 } 149 150 /// Convenience functions for common color names 151 static Color transparent() { return Color(0, 0, 0, 0); } 152 static Color white() { return Color(255, 255, 255); } ///. 153 static Color black() { return Color(0, 0, 0); } ///. 154 static Color red() { return Color(255, 0, 0); } ///. 155 static Color green() { return Color(0, 255, 0); } ///. 156 static Color blue() { return Color(0, 0, 255); } ///. 157 static Color yellow() { return Color(255, 255, 0); } ///. 158 static Color teal() { return Color(0, 255, 255); } ///. 159 static Color purple() { return Color(255, 0, 255); } ///. 160 161 /* 162 ubyte[4] toRgbaArray() { 163 return [r,g,b,a]; 164 } 165 */ 166 167 /// Makes a string that matches CSS syntax for websites 168 string toCssString() { 169 if(a == 255) 170 return "#" ~ toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b); 171 else { 172 return "rgba("~toInternal!string(r)~", "~toInternal!string(g)~", "~toInternal!string(b)~", "~toInternal!string(cast(real)a / 255.0)~")"; 173 } 174 } 175 176 /// Makes a hex string RRGGBBAA (aa only present if it is not 255) 177 string toString() { 178 if(a == 255) 179 return toCssString()[1 .. $]; 180 else 181 return toRgbaHexString(); 182 } 183 184 /// returns RRGGBBAA, even if a== 255 185 string toRgbaHexString() { 186 return toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b) ~ toHexInternal(a); 187 } 188 189 /// Gets a color by name, iff the name is one of the static members listed above 190 static Color fromNameString(string s) { 191 Color c; 192 foreach(member; __traits(allMembers, Color)) { 193 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 194 if(s == member) 195 return __traits(getMember, Color, member); 196 } 197 } 198 throw new Exception("Unknown color " ~ s); 199 } 200 201 /// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa 202 static Color fromString(string s) { 203 s = s.stripInternal(); 204 205 Color c; 206 c.a = 255; 207 208 // trying named colors via the static no-arg methods here 209 foreach(member; __traits(allMembers, Color)) { 210 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 211 if(s == member) 212 return __traits(getMember, Color, member); 213 } 214 } 215 216 // try various notations borrowed from CSS (though a little extended) 217 218 // hsl(h,s,l,a) where h is degrees and s,l,a are 0 >= x <= 1.0 219 if(s.startsWithInternal("hsl(") || s.startsWithInternal("hsla(")) { 220 assert(s[$-1] == ')'); 221 s = s[s.startsWithInternal("hsl(") ? 4 : 5 .. $ - 1]; // the closing paren 222 223 real[3] hsl; 224 ubyte a = 255; 225 226 auto parts = s.splitInternal(','); 227 foreach(i, part; parts) { 228 if(i < 3) 229 hsl[i] = toInternal!real(part.stripInternal); 230 else 231 a = cast(ubyte) (toInternal!real(part.stripInternal) * 255); 232 } 233 234 c = .fromHsl(hsl); 235 c.a = a; 236 237 return c; 238 } 239 240 // rgb(r,g,b,a) where r,g,b are 0-255 and a is 0-1.0 241 if(s.startsWithInternal("rgb(") || s.startsWithInternal("rgba(")) { 242 assert(s[$-1] == ')'); 243 s = s[s.startsWithInternal("rgb(") ? 4 : 5 .. $ - 1]; // the closing paren 244 245 auto parts = s.splitInternal(','); 246 foreach(i, part; parts) { 247 // lol the loop-switch pattern 248 auto v = toInternal!real(part.stripInternal); 249 switch(i) { 250 case 0: // red 251 c.r = cast(ubyte) v; 252 break; 253 case 1: 254 c.g = cast(ubyte) v; 255 break; 256 case 2: 257 c.b = cast(ubyte) v; 258 break; 259 case 3: 260 c.a = cast(ubyte) (v * 255); 261 break; 262 default: // ignore 263 } 264 } 265 266 return c; 267 } 268 269 270 271 272 // otherwise let's try it as a hex string, really loosely 273 274 if(s.length && s[0] == '#') 275 s = s[1 .. $]; 276 277 // not a built in... do it as a hex string 278 if(s.length >= 2) { 279 c.r = fromHexInternal(s[0 .. 2]); 280 s = s[2 .. $]; 281 } 282 if(s.length >= 2) { 283 c.g = fromHexInternal(s[0 .. 2]); 284 s = s[2 .. $]; 285 } 286 if(s.length >= 2) { 287 c.b = fromHexInternal(s[0 .. 2]); 288 s = s[2 .. $]; 289 } 290 if(s.length >= 2) { 291 c.a = fromHexInternal(s[0 .. 2]); 292 s = s[2 .. $]; 293 } 294 295 return c; 296 } 297 298 /// from hsl 299 static Color fromHsl(real h, real s, real l) { 300 return .fromHsl(h, s, l); 301 } 302 } 303 304 private string toHexInternal(ubyte b) { 305 string s; 306 if(b < 16) 307 s ~= '0'; 308 else { 309 ubyte t = (b & 0xf0) >> 4; 310 if(t >= 10) 311 s ~= 'A' + t - 10; 312 else 313 s ~= '0' + t; 314 b &= 0x0f; 315 } 316 if(b >= 10) 317 s ~= 'A' + b - 10; 318 else 319 s ~= '0' + b; 320 321 return s; 322 } 323 324 private ubyte fromHexInternal(string s) { 325 int result = 0; 326 327 int exp = 1; 328 //foreach(c; retro(s)) { // FIXME: retro doesn't work right in dtojs 329 foreach_reverse(c; s) { 330 if(c >= 'A' && c <= 'F') 331 result += exp * (c - 'A' + 10); 332 else if(c >= 'a' && c <= 'f') 333 result += exp * (c - 'a' + 10); 334 else if(c >= '0' && c <= '9') 335 result += exp * (c - '0'); 336 else 337 // throw new Exception("invalid hex character: " ~ cast(char) c); 338 return 0; 339 340 exp *= 16; 341 } 342 343 return cast(ubyte) result; 344 } 345 346 /// Converts hsl to rgb 347 Color fromHsl(real[3] hsl) { 348 return fromHsl(hsl[0], hsl[1], hsl[2]); 349 } 350 351 /// Converts hsl to rgb 352 Color fromHsl(real h, real s, real l, real a = 255) { 353 h = h % 360; 354 355 real C = (1 - absInternal(2 * l - 1)) * s; 356 357 real hPrime = h / 60; 358 359 real X = C * (1 - absInternal(hPrime % 2 - 1)); 360 361 real r, g, b; 362 363 if(h is real.nan) 364 r = g = b = 0; 365 else if (hPrime >= 0 && hPrime < 1) { 366 r = C; 367 g = X; 368 b = 0; 369 } else if (hPrime >= 1 && hPrime < 2) { 370 r = X; 371 g = C; 372 b = 0; 373 } else if (hPrime >= 2 && hPrime < 3) { 374 r = 0; 375 g = C; 376 b = X; 377 } else if (hPrime >= 3 && hPrime < 4) { 378 r = 0; 379 g = X; 380 b = C; 381 } else if (hPrime >= 4 && hPrime < 5) { 382 r = X; 383 g = 0; 384 b = C; 385 } else if (hPrime >= 5 && hPrime < 6) { 386 r = C; 387 g = 0; 388 b = X; 389 } 390 391 real m = l - C / 2; 392 393 r += m; 394 g += m; 395 b += m; 396 397 return Color( 398 cast(ubyte)(r * 255), 399 cast(ubyte)(g * 255), 400 cast(ubyte)(b * 255), 401 cast(ubyte)(a)); 402 } 403 404 /// 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. 405 real[3] toHsl(Color c, bool useWeightedLightness = false) { 406 real r1 = cast(real) c.r / 255; 407 real g1 = cast(real) c.g / 255; 408 real b1 = cast(real) c.b / 255; 409 410 real maxColor = maxInternal(r1, g1, b1); 411 real minColor = minInternal(r1, g1, b1); 412 413 real L = (maxColor + minColor) / 2 ; 414 if(useWeightedLightness) { 415 // the colors don't affect the eye equally 416 // this is a little more accurate than plain HSL numbers 417 L = 0.2126*r1 + 0.7152*g1 + 0.0722*b1; 418 } 419 real S = 0; 420 real H = 0; 421 if(maxColor != minColor) { 422 if(L < 0.5) { 423 S = (maxColor - minColor) / (maxColor + minColor); 424 } else { 425 S = (maxColor - minColor) / (2.0 - maxColor - minColor); 426 } 427 if(r1 == maxColor) { 428 H = (g1-b1) / (maxColor - minColor); 429 } else if(g1 == maxColor) { 430 H = 2.0 + (b1 - r1) / (maxColor - minColor); 431 } else { 432 H = 4.0 + (r1 - g1) / (maxColor - minColor); 433 } 434 } 435 436 H = H * 60; 437 if(H < 0){ 438 H += 360; 439 } 440 441 return [H, S, L]; 442 } 443 444 /// . 445 Color lighten(Color c, real percentage) { 446 auto hsl = toHsl(c); 447 hsl[2] *= (1 + percentage); 448 if(hsl[2] > 1) 449 hsl[2] = 1; 450 return fromHsl(hsl); 451 } 452 453 /// . 454 Color darken(Color c, real percentage) { 455 auto hsl = toHsl(c); 456 hsl[2] *= (1 - percentage); 457 return fromHsl(hsl); 458 } 459 460 /// for light colors, call darken. for dark colors, call lighten. 461 /// The goal: get toward center grey. 462 Color moderate(Color c, real percentage) { 463 auto hsl = toHsl(c); 464 if(hsl[2] > 0.5) 465 hsl[2] *= (1 - percentage); 466 else { 467 if(hsl[2] <= 0.01) // if we are given black, moderating it means getting *something* out 468 hsl[2] = percentage; 469 else 470 hsl[2] *= (1 + percentage); 471 } 472 if(hsl[2] > 1) 473 hsl[2] = 1; 474 return fromHsl(hsl); 475 } 476 477 /// the opposite of moderate. Make darks darker and lights lighter 478 Color extremify(Color c, real percentage) { 479 auto hsl = toHsl(c, true); 480 if(hsl[2] < 0.5) 481 hsl[2] *= (1 - percentage); 482 else 483 hsl[2] *= (1 + percentage); 484 if(hsl[2] > 1) 485 hsl[2] = 1; 486 return fromHsl(hsl); 487 } 488 489 /// Move around the lightness wheel, trying not to break on moderate things 490 Color oppositeLightness(Color c) { 491 auto hsl = toHsl(c); 492 493 auto original = hsl[2]; 494 495 if(original > 0.4 && original < 0.6) 496 hsl[2] = 0.8 - original; // so it isn't quite the same 497 else 498 hsl[2] = 1 - original; 499 500 return fromHsl(hsl); 501 } 502 503 /// Try to determine a text color - either white or black - based on the input 504 Color makeTextColor(Color c) { 505 auto hsl = toHsl(c, true); // give green a bonus for contrast 506 if(hsl[2] > 0.71) 507 return Color(0, 0, 0); 508 else 509 return Color(255, 255, 255); 510 } 511 512 // These provide functional access to hsl manipulation; useful if you need a delegate 513 514 Color setLightness(Color c, real lightness) { 515 auto hsl = toHsl(c); 516 hsl[2] = lightness; 517 return fromHsl(hsl); 518 } 519 520 521 522 Color rotateHue(Color c, real degrees) { 523 auto hsl = toHsl(c); 524 hsl[0] += degrees; 525 return fromHsl(hsl); 526 } 527 528 Color setHue(Color c, real hue) { 529 auto hsl = toHsl(c); 530 hsl[0] = hue; 531 return fromHsl(hsl); 532 } 533 534 Color desaturate(Color c, real percentage) { 535 auto hsl = toHsl(c); 536 hsl[1] *= (1 - percentage); 537 return fromHsl(hsl); 538 } 539 540 Color saturate(Color c, real percentage) { 541 auto hsl = toHsl(c); 542 hsl[1] *= (1 + percentage); 543 if(hsl[1] > 1) 544 hsl[1] = 1; 545 return fromHsl(hsl); 546 } 547 548 Color setSaturation(Color c, real saturation) { 549 auto hsl = toHsl(c); 550 hsl[1] = saturation; 551 return fromHsl(hsl); 552 } 553 554 555 /* 556 void main(string[] args) { 557 auto color1 = toHsl(Color(255, 0, 0)); 558 auto color = fromHsl(color1[0] + 60, color1[1], color1[2]); 559 560 writefln("#%02x%02x%02x", color.r, color.g, color.b); 561 } 562 */ 563 564 /* Color algebra functions */ 565 566 /* Alpha putpixel looks like this: 567 568 void putPixel(Image i, Color c) { 569 Color b; 570 b.r = i.data[(y * i.width + x) * bpp + 0]; 571 b.g = i.data[(y * i.width + x) * bpp + 1]; 572 b.b = i.data[(y * i.width + x) * bpp + 2]; 573 b.a = i.data[(y * i.width + x) * bpp + 3]; 574 575 float ca = cast(float) c.a / 255; 576 577 i.data[(y * i.width + x) * bpp + 0] = alpha(c.r, ca, b.r); 578 i.data[(y * i.width + x) * bpp + 1] = alpha(c.g, ca, b.g); 579 i.data[(y * i.width + x) * bpp + 2] = alpha(c.b, ca, b.b); 580 i.data[(y * i.width + x) * bpp + 3] = alpha(c.a, ca, b.a); 581 } 582 583 ubyte alpha(ubyte c1, float alpha, ubyte onto) { 584 auto got = (1 - alpha) * onto + alpha * c1; 585 586 if(got > 255) 587 return 255; 588 return cast(ubyte) got; 589 } 590 591 So, given the background color and the resultant color, what was 592 composited on to it? 593 */ 594 595 ubyte unalpha(ubyte colorYouHave, float alpha, ubyte backgroundColor) { 596 // resultingColor = (1-alpha) * backgroundColor + alpha * answer 597 auto resultingColorf = cast(float) colorYouHave; 598 auto backgroundColorf = cast(float) backgroundColor; 599 600 auto answer = (resultingColorf - backgroundColorf + alpha * backgroundColorf) / alpha; 601 if(answer > 255) 602 return 255; 603 if(answer < 0) 604 return 0; 605 return cast(ubyte) answer; 606 } 607 608 ubyte makeAlpha(ubyte colorYouHave, ubyte backgroundColor/*, ubyte foreground = 0x00*/) { 609 //auto foregroundf = cast(float) foreground; 610 auto foregroundf = 0.00f; 611 auto colorYouHavef = cast(float) colorYouHave; 612 auto backgroundColorf = cast(float) backgroundColor; 613 614 // colorYouHave = backgroundColorf - alpha * backgroundColorf + alpha * foregroundf 615 auto alphaf = 1 - colorYouHave / backgroundColorf; 616 alphaf *= 255; 617 618 if(alphaf < 0) 619 return 0; 620 if(alphaf > 255) 621 return 255; 622 return cast(ubyte) alphaf; 623 } 624 625 626 int fromHex(string s) { 627 int result = 0; 628 629 int exp = 1; 630 // foreach(c; retro(s)) { 631 foreach_reverse(c; s) { 632 if(c >= 'A' && c <= 'F') 633 result += exp * (c - 'A' + 10); 634 else if(c >= 'a' && c <= 'f') 635 result += exp * (c - 'a' + 10); 636 else if(c >= '0' && c <= '9') 637 result += exp * (c - '0'); 638 else 639 throw new Exception("invalid hex character: " ~ cast(char) c); 640 641 exp *= 16; 642 } 643 644 return result; 645 } 646 647 Color colorFromString(string s) { 648 if(s.length == 0) 649 return Color(0,0,0,255); 650 if(s[0] == '#') 651 s = s[1..$]; 652 assert(s.length == 6 || s.length == 8); 653 654 Color c; 655 656 c.r = cast(ubyte) fromHex(s[0..2]); 657 c.g = cast(ubyte) fromHex(s[2..4]); 658 c.b = cast(ubyte) fromHex(s[4..6]); 659 if(s.length == 8) 660 c.a = cast(ubyte) fromHex(s[6..8]); 661 else 662 c.a = 255; 663 664 return c; 665 } 666 667 /* 668 import browser.window; 669 import std.conv; 670 void main() { 671 import browser.document; 672 foreach(ele; document.querySelectorAll("input")) { 673 ele.addEventListener("change", { 674 auto h = toInternal!real(document.querySelector("input[name=h]").value); 675 auto s = toInternal!real(document.querySelector("input[name=s]").value); 676 auto l = toInternal!real(document.querySelector("input[name=l]").value); 677 678 Color c = Color.fromHsl(h, s, l); 679 680 auto e = document.getElementById("example"); 681 e.style.backgroundColor = c.toCssString(); 682 683 // JSElement __js_this; 684 // __js_this.style.backgroundColor = c.toCssString(); 685 }, false); 686 } 687 } 688 */ 689 690 691 692 /** 693 This provides two image classes and a bunch of functions that work on them. 694 695 Why are they separate classes? I think the operations on the two of them 696 are necessarily different. There's a whole bunch of operations that only 697 really work on truecolor (blurs, gradients), and a few that only work 698 on indexed images (palette swaps). 699 700 Even putpixel is pretty different. On indexed, it is a palette entry's 701 index number. On truecolor, it is the actual color. 702 703 A greyscale image is the weird thing in the middle. It is truecolor, but 704 fits in the same size as indexed. Still, I'd say it is a specialization 705 of truecolor. 706 707 There is a subset that works on both 708 709 */ 710 711 /// An image in memory 712 interface MemoryImage { 713 //IndexedImage convertToIndexedImage() const; 714 //TrueColorImage convertToTrueColor() const; 715 716 /// gets it as a TrueColorImage. May return this or may do a conversion and return a new image 717 TrueColorImage getAsTrueColorImage(); 718 719 /// Image width, in pixels 720 int width() const; 721 722 /// Image height, in pixels 723 int height() const; 724 } 725 726 /// An image that consists of indexes into a color palette. Use getAsTrueColorImage() if you don't care about palettes 727 class IndexedImage : MemoryImage { 728 bool hasAlpha; 729 730 /// . 731 Color[] palette; 732 /// the data as indexes into the palette. Stored left to right, top to bottom, no padding. 733 ubyte[] data; 734 735 /// . 736 override int width() const { 737 return _width; 738 } 739 740 /// . 741 override int height() const { 742 return _height; 743 } 744 745 private int _width; 746 private int _height; 747 748 /// . 749 this(int w, int h) { 750 _width = w; 751 _height = h; 752 data = new ubyte[w*h]; 753 } 754 755 /* 756 void resize(int w, int h, bool scale) { 757 758 } 759 */ 760 761 /// returns a new image 762 override TrueColorImage getAsTrueColorImage() { 763 return convertToTrueColor(); 764 } 765 766 /// Creates a new TrueColorImage based on this data 767 TrueColorImage convertToTrueColor() const { 768 auto tci = new TrueColorImage(width, height); 769 foreach(i, b; data) { 770 /* 771 if(b >= palette.length) { 772 string fuckyou; 773 fuckyou ~= b + '0'; 774 fuckyou ~= " "; 775 fuckyou ~= palette.length + '0'; 776 assert(0, fuckyou); 777 } 778 */ 779 tci.imageData.colors[i] = palette[b]; 780 } 781 return tci; 782 } 783 784 /// Gets an exact match, if possible, adds if not. See also: the findNearestColor free function. 785 ubyte getOrAddColor(Color c) { 786 foreach(i, co; palette) { 787 if(c == co) 788 return cast(ubyte) i; 789 } 790 791 return addColor(c); 792 } 793 794 /// Number of colors currently in the palette (note: palette entries are not necessarily used in the image data) 795 int numColors() const { 796 return cast(int) palette.length; 797 } 798 799 /// Adds an entry to the palette, returning its inded 800 ubyte addColor(Color c) { 801 assert(palette.length < 256); 802 if(c.a != 255) 803 hasAlpha = true; 804 palette ~= c; 805 806 return cast(ubyte) (palette.length - 1); 807 } 808 } 809 810 /// An RGBA array of image data. Use the free function quantize() to convert to an IndexedImage 811 class TrueColorImage : MemoryImage { 812 // bool hasAlpha; 813 // bool isGreyscale; 814 815 //ubyte[] data; // stored as rgba quads, upper left to right to bottom 816 /// . 817 struct Data { 818 ubyte[] bytes; /// the data as rgba bytes. Stored left to right, top to bottom, no padding. 819 // the union is no good because the length of the struct is wrong! 820 821 /// the same data as Color structs 822 @trusted // the cast here is typically unsafe, but it is ok 823 // here because I guarantee the layout, note the static assert below 824 @property inout(Color)[] colors() inout { 825 return cast(inout(Color)[]) bytes; 826 } 827 828 static assert(Color.sizeof == 4); 829 } 830 831 /// . 832 Data imageData; 833 alias imageData.bytes data; 834 835 int _width; 836 int _height; 837 838 /// . 839 override int width() const { return _width; } 840 ///. 841 override int height() const { return _height; } 842 843 /// . 844 this(int w, int h) { 845 _width = w; 846 _height = h; 847 imageData.bytes = new ubyte[w*h*4]; 848 } 849 850 /// Creates with existing data. The data pointer is stored here. 851 this(int w, int h, ubyte[] data) { 852 _width = w; 853 _height = h; 854 assert(data.length == w * h * 4); 855 imageData.bytes = data; 856 } 857 858 /// Returns this 859 override TrueColorImage getAsTrueColorImage() { 860 return this; 861 } 862 } 863 864 /// Converts true color to an indexed image. It uses palette as the starting point, adding entries 865 /// until maxColors as needed. If palette is null, it creates a whole new palette. 866 /// 867 /// After quantizing the image, it applies a dithering algorithm. 868 /// 869 /// This is not written for speed. 870 IndexedImage quantize(in TrueColorImage img, Color[] palette = null, in int maxColors = 256) 871 // this is just because IndexedImage assumes ubyte palette values 872 in { assert(maxColors <= 256); } 873 body { 874 int[Color] uses; 875 foreach(pixel; img.imageData.colors) { 876 if(auto i = pixel in uses) { 877 (*i)++; 878 } else { 879 uses[pixel] = 1; 880 } 881 } 882 883 struct ColorUse { 884 Color c; 885 int uses; 886 //string toString() { import std.conv; return c.toCssString() ~ " x " ~ to!string(uses); } 887 int opCmp(ref const ColorUse co) const { 888 return co.uses - uses; 889 } 890 } 891 892 ColorUse[] sorted; 893 894 foreach(color, count; uses) 895 sorted ~= ColorUse(color, count); 896 897 uses = null; 898 version(no_phobos) 899 sorted = sorted.sort; 900 else { 901 import std.algorithm : sort; 902 sort(sorted); 903 } 904 905 ubyte[Color] paletteAssignments; 906 foreach(idx, entry; palette) 907 paletteAssignments[entry] = cast(ubyte) idx; 908 909 // For the color assignments from the image, I do multiple passes, decreasing the acceptable 910 // distance each time until we're full. 911 912 // This is probably really slow.... but meh it gives pretty good results. 913 914 auto ddiff = 32; 915 outer: for(int d1 = 128; d1 >= 0; d1 -= ddiff) { 916 auto minDist = d1*d1; 917 if(d1 <= 64) 918 ddiff = 16; 919 if(d1 <= 32) 920 ddiff = 8; 921 foreach(possibility; sorted) { 922 if(palette.length == maxColors) 923 break; 924 if(palette.length) { 925 auto co = palette[findNearestColor(palette, possibility.c)]; 926 auto pixel = possibility.c; 927 928 auto dr = cast(int) co.r - pixel.r; 929 auto dg = cast(int) co.g - pixel.g; 930 auto db = cast(int) co.b - pixel.b; 931 932 auto dist = dr*dr + dg*dg + db*db; 933 // not good enough variety to justify an allocation yet 934 if(dist < minDist) 935 continue; 936 } 937 paletteAssignments[possibility.c] = cast(ubyte) palette.length; 938 palette ~= possibility.c; 939 } 940 } 941 942 // Final pass: just fill in any remaining space with the leftover common colors 943 while(palette.length < maxColors && sorted.length) { 944 if(sorted[0].c !in paletteAssignments) { 945 paletteAssignments[sorted[0].c] = cast(ubyte) palette.length; 946 palette ~= sorted[0].c; 947 } 948 sorted = sorted[1 .. $]; 949 } 950 951 952 bool wasPerfect = true; 953 auto newImage = new IndexedImage(img.width, img.height); 954 newImage.palette = palette; 955 foreach(idx, pixel; img.imageData.colors) { 956 if(auto p = pixel in paletteAssignments) 957 newImage.data[idx] = *p; 958 else { 959 // gotta find the closest one... 960 newImage.data[idx] = findNearestColor(palette, pixel); 961 wasPerfect = false; 962 } 963 } 964 965 if(!wasPerfect) 966 floydSteinbergDither(newImage, img); 967 968 return newImage; 969 } 970 971 /// Finds the best match for pixel in palette (currently by checking for minimum euclidean distance in rgb colorspace) 972 ubyte findNearestColor(in Color[] palette, in Color pixel) { 973 int best = 0; 974 int bestDistance = int.max; 975 foreach(pe, co; palette) { 976 auto dr = cast(int) co.r - pixel.r; 977 auto dg = cast(int) co.g - pixel.g; 978 auto db = cast(int) co.b - pixel.b; 979 int dist = dr*dr + dg*dg + db*db; 980 981 if(dist < bestDistance) { 982 best = cast(int) pe; 983 bestDistance = dist; 984 } 985 } 986 987 return cast(ubyte) best; 988 } 989 990 /+ 991 992 // Quantizing and dithering test program 993 994 void main( ){ 995 /* 996 auto img = new TrueColorImage(256, 32); 997 foreach(y; 0 .. img.height) { 998 foreach(x; 0 .. img.width) { 999 img.imageData.colors[x + y * img.width] = Color(x, y * (255 / img.height), 0); 1000 } 1001 } 1002 */ 1003 1004 TrueColorImage img; 1005 1006 { 1007 1008 import arsd.png; 1009 1010 struct P { 1011 ubyte[] range; 1012 void put(ubyte[] a) { range ~= a; } 1013 } 1014 1015 P range; 1016 import std.algorithm; 1017 1018 import std.stdio; 1019 writePngLazy(range, pngFromBytes(File("/home/me/nyesha.png").byChunk(4096)).byRgbaScanline.map!((line) { 1020 foreach(ref pixel; line.pixels) { 1021 continue; 1022 auto sum = cast(int) pixel.r + pixel.g + pixel.b; 1023 ubyte a = cast(ubyte)(sum / 3); 1024 pixel.r = a; 1025 pixel.g = a; 1026 pixel.b = a; 1027 } 1028 return line; 1029 })); 1030 1031 img = imageFromPng(readPng(range.range)).getAsTrueColorImage; 1032 1033 1034 } 1035 1036 1037 1038 auto qimg = quantize(img, null, 2); 1039 1040 import simpledisplay; 1041 auto win = new SimpleWindow(img.width, img.height * 3); 1042 auto painter = win.draw(); 1043 painter.drawImage(Point(0, 0), Image.fromMemoryImage(img)); 1044 painter.drawImage(Point(0, img.height), Image.fromMemoryImage(qimg)); 1045 floydSteinbergDither(qimg, img); 1046 painter.drawImage(Point(0, img.height * 2), Image.fromMemoryImage(qimg)); 1047 win.eventLoop(0); 1048 } 1049 +/ 1050 1051 /+ 1052 /// If the background is transparent, it simply erases the alpha channel. 1053 void removeTransparency(IndexedImage img, Color background) 1054 +/ 1055 1056 Color alphaBlend(Color foreground, Color background) { 1057 if(foreground.a != 255) 1058 foreach(idx, ref part; foreground.components) { 1059 part = cast(ubyte) (part * foreground.a / 255 + 1060 background.components[idx] * (255 - foreground.a) / 255); 1061 } 1062 1063 return foreground; 1064 } 1065 1066 /* 1067 /// Reduces the number of colors in a palette. 1068 void reducePaletteSize(IndexedImage img, int maxColors = 16) { 1069 1070 } 1071 */ 1072 1073 // I think I did this wrong... but the results aren't too bad so the bug can't be awful. 1074 /// Dithers img in place to look more like original. 1075 void floydSteinbergDither(IndexedImage img, in TrueColorImage original) { 1076 assert(img.width == original.width); 1077 assert(img.height == original.height); 1078 1079 auto buffer = new Color[](original.imageData.colors.length); 1080 1081 int x, y; 1082 1083 foreach(idx, c; original.imageData.colors) { 1084 auto n = img.palette[img.data[idx]]; 1085 int errorR = cast(int) c.r - n.r; 1086 int errorG = cast(int) c.g - n.g; 1087 int errorB = cast(int) c.b - n.b; 1088 1089 void doit(int idxOffset, int multiplier) { 1090 // if(idx + idxOffset < buffer.length) 1091 buffer[idx + idxOffset] = Color.fromIntegers( 1092 c.r + multiplier * errorR / 16, 1093 c.g + multiplier * errorG / 16, 1094 c.b + multiplier * errorB / 16, 1095 c.a 1096 ); 1097 } 1098 1099 if((x+1) != original.width) 1100 doit(1, 7); 1101 if((y+1) != original.height) { 1102 if(x != 0) 1103 doit(-1 + img.width, 3); 1104 doit(img.width, 5); 1105 if(x+1 != original.width) 1106 doit(1 + img.width, 1); 1107 } 1108 1109 img.data[idx] = findNearestColor(img.palette, buffer[idx]); 1110 1111 x++; 1112 if(x == original.width) { 1113 x = 0; 1114 y++; 1115 } 1116 } 1117 } 1118 1119 // these are just really useful in a lot of places where the color/image functions are used, 1120 // so I want them available with Color 1121 struct Point { 1122 int x; 1123 int y; 1124 } 1125 1126 struct Size { 1127 int width; 1128 int height; 1129 } 1130 1131 struct Rectangle { 1132 int left; 1133 int top; 1134 int right; 1135 int bottom; 1136 }