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 724 rgba[0] = cast(ubyte)(rgba[0] * 255 / a); 725 rgba[1] = cast(ubyte)(rgba[1] * 255 / a); 726 rgba[2] = cast(ubyte)(rgba[2] * 255 / a); 727 } 728 729 unittest { 730 Color c = Color.fromString("#fff"); 731 assert(c == Color.white); 732 assert(c == Color.fromString("#ffffff")); 733 734 c = Color.fromString("#f0f"); 735 assert(c == Color.fromString("rgb(255, 0, 255)")); 736 } 737 738 nothrow @safe 739 private string toHexInternal(ubyte b) { 740 string s; 741 if(b < 16) 742 s ~= '0'; 743 else { 744 ubyte t = (b & 0xf0) >> 4; 745 if(t >= 10) 746 s ~= 'A' + t - 10; 747 else 748 s ~= '0' + t; 749 b &= 0x0f; 750 } 751 if(b >= 10) 752 s ~= 'A' + b - 10; 753 else 754 s ~= '0' + b; 755 756 return s; 757 } 758 759 nothrow @safe @nogc pure 760 private ubyte fromHexInternal(in char[] s) { 761 int result = 0; 762 763 int exp = 1; 764 //foreach(c; retro(s)) { // FIXME: retro doesn't work right in dtojs 765 foreach_reverse(c; s) { 766 if(c >= 'A' && c <= 'F') 767 result += exp * (c - 'A' + 10); 768 else if(c >= 'a' && c <= 'f') 769 result += exp * (c - 'a' + 10); 770 else if(c >= '0' && c <= '9') 771 result += exp * (c - '0'); 772 else 773 // throw new Exception("invalid hex character: " ~ cast(char) c); 774 return 0; 775 776 exp *= 16; 777 } 778 779 return cast(ubyte) result; 780 } 781 782 /// Converts hsl to rgb 783 Color fromHsl(real[3] hsl) nothrow pure @safe @nogc { 784 return fromHsl(cast(double) hsl[0], cast(double) hsl[1], cast(double) hsl[2]); 785 } 786 787 Color fromHsl(double[3] hsl) nothrow pure @safe @nogc { 788 return fromHsl(hsl[0], hsl[1], hsl[2]); 789 } 790 791 /// Converts hsl to rgb 792 Color fromHsl(double h, double s, double l, double a = 255) nothrow pure @safe @nogc { 793 h = h % 360; 794 795 double C = (1 - absInternal(2 * l - 1)) * s; 796 797 double hPrime = h / 60; 798 799 double X = C * (1 - absInternal(hPrime % 2 - 1)); 800 801 double r, g, b; 802 803 if(h is double.nan) 804 r = g = b = 0; 805 else if (hPrime >= 0 && hPrime < 1) { 806 r = C; 807 g = X; 808 b = 0; 809 } else if (hPrime >= 1 && hPrime < 2) { 810 r = X; 811 g = C; 812 b = 0; 813 } else if (hPrime >= 2 && hPrime < 3) { 814 r = 0; 815 g = C; 816 b = X; 817 } else if (hPrime >= 3 && hPrime < 4) { 818 r = 0; 819 g = X; 820 b = C; 821 } else if (hPrime >= 4 && hPrime < 5) { 822 r = X; 823 g = 0; 824 b = C; 825 } else if (hPrime >= 5 && hPrime < 6) { 826 r = C; 827 g = 0; 828 b = X; 829 } 830 831 double m = l - C / 2; 832 833 r += m; 834 g += m; 835 b += m; 836 837 return Color( 838 cast(int)(r * 255), 839 cast(int)(g * 255), 840 cast(int)(b * 255), 841 cast(int)(a)); 842 } 843 844 /// Assumes the input `u` is already between 0 and 1 fyi. 845 nothrow pure @safe @nogc 846 double srgbToLinearRgb(double u) { 847 if(u < 0.4045) 848 return u / 12.92; 849 else 850 return pow((u + 0.055) / 1.055, 2.4); 851 // return ((u + 0.055) / 1.055) ^^ 2.4; 852 } 853 854 // could use the ^^ operator but that drags in some phobos. In this case, the import is 855 // actually insignificant, it doesn't really impact compile time, but it does complicate 856 // the dmd -v | grep std check to confirm i didn't miss any. 857 private extern(C) nothrow pure @safe @nogc double pow(double, double); 858 859 /// 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. 860 double[3] toHsl(Color c, bool useWeightedLightness = false) nothrow pure @trusted @nogc { 861 double r1 = cast(double) c.r / 255; 862 double g1 = cast(double) c.g / 255; 863 double b1 = cast(double) c.b / 255; 864 865 double maxColor = maxInternal(r1, g1, b1); 866 double minColor = minInternal(r1, g1, b1); 867 868 double L = (maxColor + minColor) / 2 ; 869 if(useWeightedLightness) { 870 // the colors don't affect the eye equally 871 // this is a little more accurate than plain HSL numbers 872 L = 0.2126*srgbToLinearRgb(r1) + 0.7152*srgbToLinearRgb(g1) + 0.0722*srgbToLinearRgb(b1); 873 // maybe a better number is 299, 587, 114 874 } 875 double S = 0; 876 double H = 0; 877 if(maxColor != minColor) { 878 if(L < 0.5) { 879 S = (maxColor - minColor) / (maxColor + minColor); 880 } else { 881 S = (maxColor - minColor) / (2.0 - maxColor - minColor); 882 } 883 if(r1 == maxColor) { 884 H = (g1-b1) / (maxColor - minColor); 885 } else if(g1 == maxColor) { 886 H = 2.0 + (b1 - r1) / (maxColor - minColor); 887 } else { 888 H = 4.0 + (r1 - g1) / (maxColor - minColor); 889 } 890 } 891 892 H = H * 60; 893 if(H < 0){ 894 H += 360; 895 } 896 897 return [H, S, L]; 898 } 899 900 /// . 901 Color lighten(Color c, double percentage) nothrow pure @safe @nogc { 902 auto hsl = toHsl(c); 903 hsl[2] *= (1 + percentage); 904 if(hsl[2] > 1) 905 hsl[2] = 1; 906 return fromHsl(hsl); 907 } 908 909 /// . 910 Color darken(Color c, double percentage) nothrow pure @safe @nogc { 911 auto hsl = toHsl(c); 912 hsl[2] *= (1 - percentage); 913 return fromHsl(hsl); 914 } 915 916 /// for light colors, call darken. for dark colors, call lighten. 917 /// The goal: get toward center grey. 918 Color moderate(Color c, double percentage) nothrow pure @safe @nogc { 919 auto hsl = toHsl(c); 920 if(hsl[2] > 0.5) 921 hsl[2] *= (1 - percentage); 922 else { 923 if(hsl[2] <= 0.01) // if we are given black, moderating it means getting *something* out 924 hsl[2] = percentage; 925 else 926 hsl[2] *= (1 + percentage); 927 } 928 if(hsl[2] > 1) 929 hsl[2] = 1; 930 return fromHsl(hsl); 931 } 932 933 /// the opposite of moderate. Make darks darker and lights lighter 934 Color extremify(Color c, double percentage) nothrow pure @safe @nogc { 935 auto hsl = toHsl(c, true); 936 if(hsl[2] < 0.5) 937 hsl[2] *= (1 - percentage); 938 else 939 hsl[2] *= (1 + percentage); 940 if(hsl[2] > 1) 941 hsl[2] = 1; 942 return fromHsl(hsl); 943 } 944 945 /// Move around the lightness wheel, trying not to break on moderate things 946 Color oppositeLightness(Color c) nothrow pure @safe @nogc { 947 auto hsl = toHsl(c); 948 949 auto original = hsl[2]; 950 951 if(original > 0.4 && original < 0.6) 952 hsl[2] = 0.8 - original; // so it isn't quite the same 953 else 954 hsl[2] = 1 - original; 955 956 return fromHsl(hsl); 957 } 958 959 /// Try to determine a text color - either white or black - based on the input 960 Color makeTextColor(Color c) nothrow pure @safe @nogc { 961 auto hsl = toHsl(c, true); // give green a bonus for contrast 962 if(hsl[2] > 0.71) 963 return Color(0, 0, 0); 964 else 965 return Color(255, 255, 255); 966 } 967 968 // These provide functional access to hsl manipulation; useful if you need a delegate 969 970 Color setLightness(Color c, double lightness) nothrow pure @safe @nogc { 971 auto hsl = toHsl(c); 972 hsl[2] = lightness; 973 return fromHsl(hsl); 974 } 975 976 977 /// 978 Color rotateHue(Color c, double degrees) nothrow pure @safe @nogc { 979 auto hsl = toHsl(c); 980 hsl[0] += degrees; 981 return fromHsl(hsl); 982 } 983 984 /// 985 Color setHue(Color c, double hue) nothrow pure @safe @nogc { 986 auto hsl = toHsl(c); 987 hsl[0] = hue; 988 return fromHsl(hsl); 989 } 990 991 /// 992 Color desaturate(Color c, double percentage) nothrow pure @safe @nogc { 993 auto hsl = toHsl(c); 994 hsl[1] *= (1 - percentage); 995 return fromHsl(hsl); 996 } 997 998 /// 999 Color saturate(Color c, double percentage) nothrow pure @safe @nogc { 1000 auto hsl = toHsl(c); 1001 hsl[1] *= (1 + percentage); 1002 if(hsl[1] > 1) 1003 hsl[1] = 1; 1004 return fromHsl(hsl); 1005 } 1006 1007 /// 1008 Color setSaturation(Color c, double saturation) nothrow pure @safe @nogc { 1009 auto hsl = toHsl(c); 1010 hsl[1] = saturation; 1011 return fromHsl(hsl); 1012 } 1013 1014 1015 /* 1016 void main(string[] args) { 1017 auto color1 = toHsl(Color(255, 0, 0)); 1018 auto color = fromHsl(color1[0] + 60, color1[1], color1[2]); 1019 1020 writefln("#%02x%02x%02x", color.r, color.g, color.b); 1021 } 1022 */ 1023 1024 /* Color algebra functions */ 1025 1026 /* Alpha putpixel looks like this: 1027 1028 void putPixel(Image i, Color c) { 1029 Color b; 1030 b.r = i.data[(y * i.width + x) * bpp + 0]; 1031 b.g = i.data[(y * i.width + x) * bpp + 1]; 1032 b.b = i.data[(y * i.width + x) * bpp + 2]; 1033 b.a = i.data[(y * i.width + x) * bpp + 3]; 1034 1035 float ca = cast(float) c.a / 255; 1036 1037 i.data[(y * i.width + x) * bpp + 0] = alpha(c.r, ca, b.r); 1038 i.data[(y * i.width + x) * bpp + 1] = alpha(c.g, ca, b.g); 1039 i.data[(y * i.width + x) * bpp + 2] = alpha(c.b, ca, b.b); 1040 i.data[(y * i.width + x) * bpp + 3] = alpha(c.a, ca, b.a); 1041 } 1042 1043 ubyte alpha(ubyte c1, float alpha, ubyte onto) { 1044 auto got = (1 - alpha) * onto + alpha * c1; 1045 1046 if(got > 255) 1047 return 255; 1048 return cast(ubyte) got; 1049 } 1050 1051 So, given the background color and the resultant color, what was 1052 composited on to it? 1053 */ 1054 1055 /// 1056 ubyte unalpha(ubyte colorYouHave, float alpha, ubyte backgroundColor) nothrow pure @safe @nogc { 1057 // resultingColor = (1-alpha) * backgroundColor + alpha * answer 1058 auto resultingColorf = cast(float) colorYouHave; 1059 auto backgroundColorf = cast(float) backgroundColor; 1060 1061 auto answer = (resultingColorf - backgroundColorf + alpha * backgroundColorf) / alpha; 1062 return Color.clampToByte(cast(int) answer); 1063 } 1064 1065 /// 1066 ubyte makeAlpha(ubyte colorYouHave, ubyte backgroundColor/*, ubyte foreground = 0x00*/) nothrow pure @safe @nogc { 1067 //auto foregroundf = cast(float) foreground; 1068 auto foregroundf = 0.00f; 1069 auto colorYouHavef = cast(float) colorYouHave; 1070 auto backgroundColorf = cast(float) backgroundColor; 1071 1072 // colorYouHave = backgroundColorf - alpha * backgroundColorf + alpha * foregroundf 1073 auto alphaf = 1 - colorYouHave / backgroundColorf; 1074 alphaf *= 255; 1075 1076 return Color.clampToByte(cast(int) alphaf); 1077 } 1078 1079 1080 int fromHex(string s) { 1081 int result = 0; 1082 1083 int exp = 1; 1084 // foreach(c; retro(s)) { 1085 foreach_reverse(c; s) { 1086 if(c >= 'A' && c <= 'F') 1087 result += exp * (c - 'A' + 10); 1088 else if(c >= 'a' && c <= 'f') 1089 result += exp * (c - 'a' + 10); 1090 else if(c >= '0' && c <= '9') 1091 result += exp * (c - '0'); 1092 else 1093 throw new Exception("invalid hex character: " ~ cast(char) c); 1094 1095 exp *= 16; 1096 } 1097 1098 return result; 1099 } 1100 1101 /// 1102 Color colorFromString(string s) { 1103 if(s.length == 0) 1104 return Color(0,0,0,255); 1105 if(s[0] == '#') 1106 s = s[1..$]; 1107 assert(s.length == 6 || s.length == 8); 1108 1109 Color c; 1110 1111 c.r = cast(ubyte) fromHex(s[0..2]); 1112 c.g = cast(ubyte) fromHex(s[2..4]); 1113 c.b = cast(ubyte) fromHex(s[4..6]); 1114 if(s.length == 8) 1115 c.a = cast(ubyte) fromHex(s[6..8]); 1116 else 1117 c.a = 255; 1118 1119 return c; 1120 } 1121 1122 /* 1123 import browser.window; 1124 void main() { 1125 import browser.document; 1126 foreach(ele; document.querySelectorAll("input")) { 1127 ele.addEventListener("change", { 1128 auto h = toInternal!double(document.querySelector("input[name=h]").value); 1129 auto s = toInternal!double(document.querySelector("input[name=s]").value); 1130 auto l = toInternal!double(document.querySelector("input[name=l]").value); 1131 1132 Color c = Color.fromHsl(h, s, l); 1133 1134 auto e = document.getElementById("example"); 1135 e.style.backgroundColor = c.toCssString(); 1136 1137 // JSElement __js_this; 1138 // __js_this.style.backgroundColor = c.toCssString(); 1139 }, false); 1140 } 1141 } 1142 */ 1143 1144 1145 1146 /** 1147 This provides two image classes and a bunch of functions that work on them. 1148 1149 Why are they separate classes? I think the operations on the two of them 1150 are necessarily different. There's a whole bunch of operations that only 1151 really work on truecolor (blurs, gradients), and a few that only work 1152 on indexed images (palette swaps). 1153 1154 Even putpixel is pretty different. On indexed, it is a palette entry's 1155 index number. On truecolor, it is the actual color. 1156 1157 A greyscale image is the weird thing in the middle. It is truecolor, but 1158 fits in the same size as indexed. Still, I'd say it is a specialization 1159 of truecolor. 1160 1161 There is a subset that works on both 1162 1163 */ 1164 1165 /// An image in memory 1166 interface MemoryImage { 1167 //IndexedImage convertToIndexedImage() const; 1168 //TrueColorImage convertToTrueColor() const; 1169 1170 /// gets it as a TrueColorImage. May return this or may do a conversion and return a new image 1171 TrueColorImage getAsTrueColorImage() pure nothrow @safe; 1172 1173 /// Image width, in pixels 1174 int width() const pure nothrow @safe @nogc; 1175 1176 /// Image height, in pixels 1177 int height() const pure nothrow @safe @nogc; 1178 1179 /// Get image pixel. Slow, but returns valid RGBA color (completely transparent for off-image pixels). 1180 Color getPixel(int x, int y) const pure nothrow @safe @nogc; 1181 1182 /// Set image pixel. 1183 void setPixel(int x, int y, in Color clr) nothrow @safe; 1184 1185 /// Returns a copy of the image 1186 MemoryImage clone() const pure nothrow @safe; 1187 1188 /// Load image from file. This will import arsd.image to do the actual work, and cost nothing if you don't use it. 1189 static MemoryImage fromImage(T : const(char)[]) (T filename) @trusted { 1190 static if (__traits(compiles, (){import arsd.image;})) { 1191 // yay, we have image loader here, try it! 1192 import arsd.image; 1193 return loadImageFromFile(filename); 1194 } else { 1195 static assert(0, "please provide 'arsd.image' to load images!"); 1196 } 1197 } 1198 1199 // ***This method is deliberately not publicly documented.*** 1200 // What it does is unconditionally frees internal image storage, without any sanity checks. 1201 // If you will do this, make sure that you have no references to image data left (like 1202 // slices of [data] array, for example). Those references will become invalid, and WILL 1203 // lead to Undefined Behavior. 1204 // tl;dr: IF YOU HAVE *ANY* QUESTIONS REGARDING THIS COMMENT, DON'T USE THIS! 1205 // Note to implementors: it is safe to simply do nothing in this method. 1206 // Also, it should be safe to call this method twice or more. 1207 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 1208 1209 /// Convenient alias for `fromImage` 1210 alias fromImageFile = fromImage; 1211 } 1212 1213 /// An image that consists of indexes into a color palette. Use [getAsTrueColorImage]() if you don't care about palettes 1214 class IndexedImage : MemoryImage { 1215 bool hasAlpha; 1216 1217 /// . 1218 Color[] palette; 1219 /// the data as indexes into the palette. Stored left to right, top to bottom, no padding. 1220 ubyte[] data; 1221 1222 override void clearInternal () nothrow @system {// @nogc { 1223 import core.memory : GC; 1224 // it is safe to call [GC.free] with `null` pointer. 1225 GC.free(GC.addrOf(palette.ptr)); palette = null; 1226 GC.free(GC.addrOf(data.ptr)); data = null; 1227 _width = _height = 0; 1228 } 1229 1230 /// . 1231 override int width() const pure nothrow @safe @nogc { 1232 return _width; 1233 } 1234 1235 /// . 1236 override int height() const pure nothrow @safe @nogc { 1237 return _height; 1238 } 1239 1240 /// . 1241 override IndexedImage clone() const pure nothrow @trusted { 1242 auto n = new IndexedImage(width, height); 1243 n.data[] = this.data[]; // the data member is already there, so array copy 1244 n.palette = this.palette.dup; // and here we need to allocate too, so dup 1245 n.hasAlpha = this.hasAlpha; 1246 return n; 1247 } 1248 1249 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1250 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1251 size_t pos = cast(size_t)y*_width+x; 1252 if (pos >= data.length) return Color(0, 0, 0, 0); 1253 ubyte b = data.ptr[pos]; 1254 if (b >= palette.length) return Color(0, 0, 0, 0); 1255 return palette.ptr[b]; 1256 } else { 1257 return Color(0, 0, 0, 0); 1258 } 1259 } 1260 1261 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1262 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1263 size_t pos = cast(size_t)y*_width+x; 1264 if (pos >= data.length) return; 1265 ubyte pidx = findNearestColor(palette, clr); 1266 if (palette.length < 255 && 1267 (palette.ptr[pidx].r != clr.r || palette.ptr[pidx].g != clr.g || palette.ptr[pidx].b != clr.b || palette.ptr[pidx].a != clr.a)) { 1268 // add new color 1269 pidx = addColor(clr); 1270 } 1271 data.ptr[pos] = pidx; 1272 } 1273 } 1274 1275 private int _width; 1276 private int _height; 1277 1278 /// . 1279 this(int w, int h) pure nothrow @safe { 1280 _width = w; 1281 _height = h; 1282 1283 // ensure that the computed size does not exceed basic address space limits 1284 assert(cast(ulong)w * h <= size_t.max); 1285 // upcast to avoid overflow for images larger than 536 Mpix 1286 data = new ubyte[cast(size_t)w*h]; 1287 } 1288 1289 /* 1290 void resize(int w, int h, bool scale) { 1291 1292 } 1293 */ 1294 1295 /// returns a new image 1296 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1297 return convertToTrueColor(); 1298 } 1299 1300 /// Creates a new TrueColorImage based on this data 1301 TrueColorImage convertToTrueColor() const pure nothrow @trusted { 1302 auto tci = new TrueColorImage(width, height); 1303 foreach(i, b; data) { 1304 tci.imageData.colors[i] = palette[b]; 1305 } 1306 return tci; 1307 } 1308 1309 /// Gets an exact match, if possible, adds if not. See also: the findNearestColor free function. 1310 ubyte getOrAddColor(Color c) nothrow @trusted { 1311 foreach(i, co; palette) { 1312 if(c == co) 1313 return cast(ubyte) i; 1314 } 1315 1316 return addColor(c); 1317 } 1318 1319 /// Number of colors currently in the palette (note: palette entries are not necessarily used in the image data) 1320 int numColors() const pure nothrow @trusted @nogc { 1321 return cast(int) palette.length; 1322 } 1323 1324 /// Adds an entry to the palette, returning its index 1325 ubyte addColor(Color c) nothrow @trusted { 1326 assert(palette.length < 256); 1327 if(c.a != 255) 1328 hasAlpha = true; 1329 palette ~= c; 1330 1331 return cast(ubyte) (palette.length - 1); 1332 } 1333 } 1334 1335 /// An RGBA array of image data. Use the free function quantize() to convert to an IndexedImage 1336 class TrueColorImage : MemoryImage { 1337 // bool hasAlpha; 1338 // bool isGreyscale; 1339 1340 //ubyte[] data; // stored as rgba quads, upper left to right to bottom 1341 /// . 1342 struct Data { 1343 ubyte[] bytes; /// the data as rgba bytes. Stored left to right, top to bottom, no padding. 1344 // the union is no good because the length of the struct is wrong! 1345 1346 /// the same data as Color structs 1347 @trusted // the cast here is typically unsafe, but it is ok 1348 // here because I guarantee the layout, note the static assert below 1349 @property inout(Color)[] colors() inout pure nothrow @nogc { 1350 return cast(inout(Color)[]) bytes; 1351 } 1352 1353 static assert(Color.sizeof == 4); 1354 } 1355 1356 /// . 1357 Data imageData; 1358 alias imageData.bytes data; 1359 1360 int _width; 1361 int _height; 1362 1363 override void clearInternal () nothrow @system {// @nogc { 1364 import core.memory : GC; 1365 // it is safe to call [GC.free] with `null` pointer. 1366 GC.free(GC.addrOf(imageData.bytes.ptr)); imageData.bytes = null; 1367 _width = _height = 0; 1368 } 1369 1370 /// . 1371 override TrueColorImage clone() const pure nothrow @trusted { 1372 auto n = new TrueColorImage(width, height); 1373 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1374 return n; 1375 } 1376 1377 /// . 1378 override int width() const pure nothrow @trusted @nogc { return _width; } 1379 ///. 1380 override int height() const pure nothrow @trusted @nogc { return _height; } 1381 1382 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1383 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1384 size_t pos = cast(size_t)y*_width+x; 1385 return imageData.colors.ptr[pos]; 1386 } else { 1387 return Color(0, 0, 0, 0); 1388 } 1389 } 1390 1391 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1392 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1393 size_t pos = cast(size_t)y*_width+x; 1394 if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1395 } 1396 } 1397 1398 /// . 1399 this(int w, int h) pure nothrow @safe { 1400 _width = w; 1401 _height = h; 1402 1403 // ensure that the computed size does not exceed basic address space limits 1404 assert(cast(ulong)w * h * 4 <= size_t.max); 1405 // upcast to avoid overflow for images larger than 536 Mpix 1406 imageData.bytes = new ubyte[cast(size_t)w * h * 4]; 1407 } 1408 1409 /// Creates with existing data. The data pointer is stored here. 1410 this(int w, int h, ubyte[] data) pure nothrow @safe { 1411 _width = w; 1412 _height = h; 1413 assert(cast(ulong)w * h * 4 <= size_t.max); 1414 assert(data.length == cast(size_t)w * h * 4); 1415 imageData.bytes = data; 1416 } 1417 1418 /// Returns this 1419 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1420 return this; 1421 } 1422 } 1423 1424 /+ 1425 /// An RGB array of image data. 1426 class TrueColorImageWithoutAlpha : MemoryImage { 1427 struct Data { 1428 ubyte[] bytes; // the data as rgba bytes. Stored left to right, top to bottom, no padding. 1429 } 1430 1431 /// . 1432 Data imageData; 1433 1434 int _width; 1435 int _height; 1436 1437 override void clearInternal () nothrow @system {// @nogc { 1438 import core.memory : GC; 1439 // it is safe to call [GC.free] with `null` pointer. 1440 GC.free(imageData.bytes.ptr); imageData.bytes = null; 1441 _width = _height = 0; 1442 } 1443 1444 /// . 1445 override TrueColorImageWithoutAlpha clone() const pure nothrow @trusted { 1446 auto n = new TrueColorImageWithoutAlpha(width, height); 1447 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1448 return n; 1449 } 1450 1451 /// . 1452 override int width() const pure nothrow @trusted @nogc { return _width; } 1453 ///. 1454 override int height() const pure nothrow @trusted @nogc { return _height; } 1455 1456 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1457 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1458 uint pos = (y*_width+x) * 3; 1459 return Color(imageData.bytes[0], imageData.bytes[1], imageData.bytes[2], 255); 1460 } else { 1461 return Color(0, 0, 0, 0); 1462 } 1463 } 1464 1465 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1466 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1467 uint pos = y*_width+x; 1468 //if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1469 // FIXME 1470 } 1471 } 1472 1473 /// . 1474 this(int w, int h) pure nothrow @safe { 1475 _width = w; 1476 _height = h; 1477 imageData.bytes = new ubyte[w*h*3]; 1478 } 1479 1480 /// Creates with existing data. The data pointer is stored here. 1481 this(int w, int h, ubyte[] data) pure nothrow @safe { 1482 _width = w; 1483 _height = h; 1484 assert(data.length == w * h * 3); 1485 imageData.bytes = data; 1486 } 1487 1488 /// 1489 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1490 // FIXME 1491 //return this; 1492 } 1493 } 1494 +/ 1495 1496 1497 alias extern(C) int function(scope const void*, scope const void*) @system Comparator; 1498 @trusted void nonPhobosSort(T)(T[] obj, Comparator comparator) { 1499 import core.stdc.stdlib; 1500 qsort(obj.ptr, obj.length, typeof(obj[0]).sizeof, comparator); 1501 } 1502 1503 /// Converts true color to an indexed image. It uses palette as the starting point, adding entries 1504 /// until maxColors as needed. If palette is null, it creates a whole new palette. 1505 /// 1506 /// After quantizing the image, it applies a dithering algorithm. 1507 /// 1508 /// This is not written for speed. 1509 IndexedImage quantize(in TrueColorImage img, Color[] palette = null, in int maxColors = 256) 1510 // this is just because IndexedImage assumes ubyte palette values 1511 in { assert(maxColors <= 256); } 1512 do { 1513 int[Color] uses; 1514 foreach(pixel; img.imageData.colors) { 1515 if(auto i = pixel in uses) { 1516 (*i)++; 1517 } else { 1518 uses[pixel] = 1; 1519 } 1520 } 1521 1522 struct ColorUse { 1523 Color c; 1524 int uses; 1525 //string toString() { return c.toCssString() ~ " x " ~ to!string(uses); } 1526 int opCmp(ref const ColorUse co) const { 1527 return co.uses - uses; 1528 } 1529 extern(C) static int comparator(scope const void* lhs, scope const void* rhs) { 1530 return (cast(ColorUse*)rhs).uses - (cast(ColorUse*)lhs).uses; 1531 } 1532 } 1533 1534 ColorUse[] sorted; 1535 1536 foreach(color, count; uses) 1537 sorted ~= ColorUse(color, count); 1538 1539 uses = null; 1540 1541 nonPhobosSort(sorted, &ColorUse.comparator); 1542 // or, with phobos, but that adds 70ms to compile time 1543 //import std.algorithm.sorting : sort; 1544 //sort(sorted); 1545 1546 ubyte[Color] paletteAssignments; 1547 foreach(idx, entry; palette) 1548 paletteAssignments[entry] = cast(ubyte) idx; 1549 1550 // For the color assignments from the image, I do multiple passes, decreasing the acceptable 1551 // distance each time until we're full. 1552 1553 // This is probably really slow.... but meh it gives pretty good results. 1554 1555 auto ddiff = 32; 1556 outer: for(int d1 = 128; d1 >= 0; d1 -= ddiff) { 1557 auto minDist = d1*d1; 1558 if(d1 <= 64) 1559 ddiff = 16; 1560 if(d1 <= 32) 1561 ddiff = 8; 1562 foreach(possibility; sorted) { 1563 if(palette.length == maxColors) 1564 break; 1565 if(palette.length) { 1566 auto co = palette[findNearestColor(palette, possibility.c)]; 1567 auto pixel = possibility.c; 1568 1569 auto dr = cast(int) co.r - pixel.r; 1570 auto dg = cast(int) co.g - pixel.g; 1571 auto db = cast(int) co.b - pixel.b; 1572 1573 auto dist = dr*dr + dg*dg + db*db; 1574 // not good enough variety to justify an allocation yet 1575 if(dist < minDist) 1576 continue; 1577 } 1578 paletteAssignments[possibility.c] = cast(ubyte) palette.length; 1579 palette ~= possibility.c; 1580 } 1581 } 1582 1583 // Final pass: just fill in any remaining space with the leftover common colors 1584 while(palette.length < maxColors && sorted.length) { 1585 if(sorted[0].c !in paletteAssignments) { 1586 paletteAssignments[sorted[0].c] = cast(ubyte) palette.length; 1587 palette ~= sorted[0].c; 1588 } 1589 sorted = sorted[1 .. $]; 1590 } 1591 1592 1593 bool wasPerfect = true; 1594 auto newImage = new IndexedImage(img.width, img.height); 1595 newImage.palette = palette; 1596 foreach(idx, pixel; img.imageData.colors) { 1597 if(auto p = pixel in paletteAssignments) 1598 newImage.data[idx] = *p; 1599 else { 1600 // gotta find the closest one... 1601 newImage.data[idx] = findNearestColor(palette, pixel); 1602 wasPerfect = false; 1603 } 1604 } 1605 1606 if(!wasPerfect) 1607 floydSteinbergDither(newImage, img); 1608 1609 return newImage; 1610 } 1611 1612 /// Finds the best match for pixel in palette (currently by checking for minimum euclidean distance in rgb colorspace) 1613 ubyte findNearestColor(in Color[] palette, in Color pixel) nothrow pure @trusted @nogc { 1614 int best = 0; 1615 int bestDistance = int.max; 1616 foreach(pe, co; palette) { 1617 auto dr = cast(int) co.r - pixel.r; 1618 auto dg = cast(int) co.g - pixel.g; 1619 auto db = cast(int) co.b - pixel.b; 1620 int dist = dr*dr + dg*dg + db*db; 1621 1622 if(dist < bestDistance) { 1623 best = cast(int) pe; 1624 bestDistance = dist; 1625 } 1626 } 1627 1628 return cast(ubyte) best; 1629 } 1630 1631 /+ 1632 1633 // Quantizing and dithering test program 1634 1635 void main( ){ 1636 /* 1637 auto img = new TrueColorImage(256, 32); 1638 foreach(y; 0 .. img.height) { 1639 foreach(x; 0 .. img.width) { 1640 img.imageData.colors[x + y * img.width] = Color(x, y * (255 / img.height), 0); 1641 } 1642 } 1643 */ 1644 1645 TrueColorImage img; 1646 1647 { 1648 1649 import arsd.png; 1650 1651 struct P { 1652 ubyte[] range; 1653 void put(ubyte[] a) { range ~= a; } 1654 } 1655 1656 P range; 1657 import std.algorithm; // commented out 1658 1659 import std.stdio; // commented out 1660 writePngLazy(range, pngFromBytes(File("/home/me/nyesha.png").byChunk(4096)).byRgbaScanline.map!((line) { 1661 foreach(ref pixel; line.pixels) { 1662 continue; 1663 auto sum = cast(int) pixel.r + pixel.g + pixel.b; 1664 ubyte a = cast(ubyte)(sum / 3); 1665 pixel.r = a; 1666 pixel.g = a; 1667 pixel.b = a; 1668 } 1669 return line; 1670 })); 1671 1672 img = imageFromPng(readPng(range.range)).getAsTrueColorImage; 1673 1674 1675 } 1676 1677 1678 1679 auto qimg = quantize(img, null, 2); 1680 1681 import arsd.simpledisplay; 1682 auto win = new SimpleWindow(img.width, img.height * 3); 1683 auto painter = win.draw(); 1684 painter.drawImage(Point(0, 0), Image.fromMemoryImage(img)); 1685 painter.drawImage(Point(0, img.height), Image.fromMemoryImage(qimg)); 1686 floydSteinbergDither(qimg, img); 1687 painter.drawImage(Point(0, img.height * 2), Image.fromMemoryImage(qimg)); 1688 win.eventLoop(0); 1689 } 1690 +/ 1691 1692 /+ 1693 /// If the background is transparent, it simply erases the alpha channel. 1694 void removeTransparency(IndexedImage img, Color background) 1695 +/ 1696 1697 /// Perform alpha-blending of `fore` to this color, return new color. 1698 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 1699 Color alphaBlend(Color foreground, Color background) pure nothrow @safe @nogc { 1700 //if(foreground.a == 255) 1701 //return foreground; 1702 if(foreground.a == 0) 1703 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) 1704 1705 static if (__VERSION__ > 2067) pragma(inline, true); 1706 return background.alphaBlend(foreground); 1707 } 1708 1709 /* 1710 /// Reduces the number of colors in a palette. 1711 void reducePaletteSize(IndexedImage img, int maxColors = 16) { 1712 1713 } 1714 */ 1715 1716 // I think I did this wrong... but the results aren't too bad so the bug can't be awful. 1717 /// Dithers img in place to look more like original. 1718 void floydSteinbergDither(IndexedImage img, in TrueColorImage original) nothrow @trusted { 1719 assert(img.width == original.width); 1720 assert(img.height == original.height); 1721 1722 auto buffer = new Color[](original.imageData.colors.length); 1723 1724 int x, y; 1725 1726 foreach(idx, c; original.imageData.colors) { 1727 auto n = img.palette[img.data[idx]]; 1728 int errorR = cast(int) c.r - n.r; 1729 int errorG = cast(int) c.g - n.g; 1730 int errorB = cast(int) c.b - n.b; 1731 1732 void doit(int idxOffset, int multiplier) { 1733 // if(idx + idxOffset < buffer.length) 1734 buffer[idx + idxOffset] = Color.fromIntegers( 1735 c.r + multiplier * errorR / 16, 1736 c.g + multiplier * errorG / 16, 1737 c.b + multiplier * errorB / 16, 1738 c.a 1739 ); 1740 } 1741 1742 if((x+1) != original.width) 1743 doit(1, 7); 1744 if((y+1) != original.height) { 1745 if(x != 0) 1746 doit(-1 + img.width, 3); 1747 doit(img.width, 5); 1748 if(x+1 != original.width) 1749 doit(1 + img.width, 1); 1750 } 1751 1752 img.data[idx] = findNearestColor(img.palette, buffer[idx]); 1753 1754 x++; 1755 if(x == original.width) { 1756 x = 0; 1757 y++; 1758 } 1759 } 1760 } 1761 1762 // these are just really useful in a lot of places where the color/image functions are used, 1763 // so I want them available with Color 1764 /// 1765 struct Point { 1766 int x; /// 1767 int y; /// 1768 1769 pure const nothrow @safe: 1770 1771 Point opBinary(string op)(in Point rhs) @nogc { 1772 return Point(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); 1773 } 1774 1775 Point opBinary(string op)(int rhs) @nogc { 1776 return Point(mixin("x" ~ op ~ "rhs"), mixin("y" ~ op ~ "rhs")); 1777 } 1778 } 1779 1780 /// 1781 struct Size { 1782 int width; /// 1783 int height; /// 1784 1785 int area() pure nothrow @safe const @nogc { return width * height; } 1786 } 1787 1788 /// 1789 struct Rectangle { 1790 int left; /// 1791 int top; /// 1792 int right; /// 1793 int bottom; /// 1794 1795 pure const nothrow @safe @nogc: 1796 1797 /// 1798 this(int left, int top, int right, int bottom) { 1799 this.left = left; 1800 this.top = top; 1801 this.right = right; 1802 this.bottom = bottom; 1803 } 1804 1805 /// 1806 this(in Point upperLeft, in Point lowerRight) { 1807 this(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y); 1808 } 1809 1810 /// 1811 this(in Point upperLeft, in Size size) { 1812 this(upperLeft.x, upperLeft.y, upperLeft.x + size.width, upperLeft.y + size.height); 1813 } 1814 1815 /// 1816 @property Point upperLeft() { 1817 return Point(left, top); 1818 } 1819 1820 /// 1821 @property Point upperRight() { 1822 return Point(right, top); 1823 } 1824 1825 /// 1826 @property Point lowerLeft() { 1827 return Point(left, bottom); 1828 } 1829 1830 /// 1831 @property Point lowerRight() { 1832 return Point(right, bottom); 1833 } 1834 1835 /// 1836 @property Point center() { 1837 return Point((right + left) / 2, (bottom + top) / 2); 1838 } 1839 1840 /// 1841 @property Size size() { 1842 return Size(width, height); 1843 } 1844 1845 /// 1846 @property int width() { 1847 return right - left; 1848 } 1849 1850 /// 1851 @property int height() { 1852 return bottom - top; 1853 } 1854 1855 /// Returns true if this rectangle entirely contains the other 1856 bool contains(in Rectangle r) { 1857 return contains(r.upperLeft) && contains(r.lowerRight); 1858 } 1859 1860 /// ditto 1861 bool contains(in Point p) { 1862 return (p.x >= left && p.x < right && p.y >= top && p.y < bottom); 1863 } 1864 1865 /// Returns true of the two rectangles at any point overlap 1866 bool overlaps(in Rectangle r) { 1867 // the -1 in here are because right and top are exclusive 1868 return !((right-1) < r.left || (r.right-1) < left || (bottom-1) < r.top || (r.bottom-1) < top); 1869 } 1870 1871 /++ 1872 Returns a Rectangle representing the intersection of this and the other given one. 1873 1874 History: 1875 Added July 1, 2021 1876 +/ 1877 Rectangle intersectionOf(in Rectangle r) { 1878 auto tmp = Rectangle(max(left, r.left), max(top, r.top), min(right, r.right), min(bottom, r.bottom)); 1879 if(tmp.left >= tmp.right || tmp.top >= tmp.bottom) 1880 tmp = Rectangle.init; 1881 1882 return tmp; 1883 } 1884 } 1885 1886 private int max(int a, int b) @nogc nothrow pure @safe { 1887 return a >= b ? a : b; 1888 } 1889 private int min(int a, int b) @nogc nothrow pure @safe { 1890 return a <= b ? a : b; 1891 } 1892 1893 /++ 1894 Implements a flood fill algorithm, like the bucket tool in 1895 MS Paint. 1896 1897 Note it assumes `what.length == width*height`. 1898 1899 Params: 1900 what = the canvas to work with, arranged as top to bottom, left to right elements 1901 width = the width of the canvas 1902 height = the height of the canvas 1903 target = the type to replace. You may pass the existing value if you want to do what Paint does 1904 replacement = the replacement value 1905 x = the x-coordinate to start the fill (think of where the user clicked in Paint) 1906 y = the y-coordinate to start the fill 1907 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. 1908 +/ 1909 void floodFill(T)( 1910 T[] what, int width, int height, // the canvas to inspect 1911 T target, T replacement, // fill params 1912 int x, int y, bool delegate(int x, int y) @safe additionalCheck) // the node 1913 1914 // in(what.length == width * height) // gdc doesn't support this syntax yet so not gonna use it until that comes out. 1915 { 1916 assert(what.length == width * height); // will use the contract above when gdc supports it 1917 1918 T node = what[y * width + x]; 1919 1920 if(target == replacement) return; 1921 1922 if(node != target) return; 1923 1924 if(additionalCheck is null) 1925 additionalCheck = (int, int) => true; 1926 1927 if(!additionalCheck(x, y)) 1928 return; 1929 1930 Point[] queue; 1931 1932 queue ~= Point(x, y); 1933 1934 while(queue.length) { 1935 auto n = queue[0]; 1936 queue = queue[1 .. $]; 1937 //queue.assumeSafeAppend(); // lol @safe breakage 1938 1939 auto w = n; 1940 int offset = cast(int) (n.y * width + n.x); 1941 auto e = n; 1942 auto eoffset = offset; 1943 w.x--; 1944 offset--; 1945 while(w.x >= 0 && what[offset] == target && additionalCheck(w.x, w.y)) { 1946 w.x--; 1947 offset--; 1948 } 1949 while(e.x < width && what[eoffset] == target && additionalCheck(e.x, e.y)) { 1950 e.x++; 1951 eoffset++; 1952 } 1953 1954 // to make it inclusive again 1955 w.x++; 1956 offset++; 1957 foreach(o ; offset .. eoffset) { 1958 what[o] = replacement; 1959 if(w.y && what[o - width] == target && additionalCheck(w.x, w.y)) 1960 queue ~= Point(w.x, w.y - 1); 1961 if(w.y + 1 < height && what[o + width] == target && additionalCheck(w.x, w.y)) 1962 queue ~= Point(w.x, w.y + 1); 1963 w.x++; 1964 } 1965 } 1966 1967 /+ 1968 what[y * width + x] = replacement; 1969 1970 if(x) 1971 floodFill(what, width, height, target, replacement, 1972 x - 1, y, additionalCheck); 1973 1974 if(x != width - 1) 1975 floodFill(what, width, height, target, replacement, 1976 x + 1, y, additionalCheck); 1977 1978 if(y) 1979 floodFill(what, width, height, target, replacement, 1980 x, y - 1, additionalCheck); 1981 1982 if(y != height - 1) 1983 floodFill(what, width, height, target, replacement, 1984 x, y + 1, additionalCheck); 1985 +/ 1986 } 1987 1988 // for scripting, so you can tag it without strictly needing to import arsd.jsvar 1989 enum arsd_jsvar_compatible = "arsd_jsvar_compatible";