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