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