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