1 /** 2 * Copyright: Copyright Auburn Sounds 2015 - 2016. 3 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 4 * Authors: Guillaume Piolat 5 */ 6 module dplug.graphics.drawex; 7 8 // Extends ae.graphics.utils 9 // Additional graphics primitives, and image loading 10 11 import core.stdc.stdlib: free; 12 13 import std.algorithm.comparison; 14 import std.math; 15 import std.traits; 16 17 import gfm.math.box; 18 19 import dplug.core.nogc; 20 import dplug.core.alignedbuffer; 21 22 import dplug.graphics.view; 23 import dplug.graphics.draw; 24 import dplug.graphics.image; 25 import dplug.graphics.pngload; 26 27 nothrow: 28 @nogc: 29 30 31 /// Crop a view from a box2i 32 auto crop(V)(auto ref V src, box2i b) if (isView!V) 33 { 34 return dplug.graphics.view.crop(src, b.min.x, b.min.y, b.max.x, b.max.y); 35 } 36 37 /// Crop an ImageRef and get an ImageRef instead of a Voldemort type. 38 /// This also avoid adding offset to coordinates. 39 ImageRef!COLOR cropImageRef(COLOR)(ImageRef!COLOR src, box2i rect) 40 { 41 ImageRef!COLOR result; 42 result.w = rect.width; 43 result.h = rect.height; 44 result.pitch = src.pitch; 45 COLOR[] scan = src.scanline(rect.min.y); 46 result.pixels = &scan[rect.min.x]; 47 return result; 48 } 49 50 /// Rough anti-aliased fillsector 51 void aaFillSector(V, COLOR)(auto ref V v, float x, float y, float r0, float r1, float a0, float a1, COLOR c) 52 if (isWritableView!V && is(COLOR : ViewColor!V)) 53 { 54 alias ChannelType = COLOR.ChannelType; 55 56 if (a0 == a1) 57 return; 58 59 int x0 = cast(int)floor(x - r1 - 1); 60 int x1 = cast(int)ceil(x + r1 + 1); 61 62 int y0 = cast(int)floor(y - r1 - 1); 63 int y1 = cast(int)ceil(y + r1 + 1); 64 65 float r0s = std.algorithm.max(0, r0 - 1) ^^ 2; 66 float r1s = (r1 + 1) * (r1 + 1); 67 68 69 if (a0 > a1) 70 a1 += 2 * PI; 71 72 int xmin = x0; 73 int xmax = x1+1; 74 int ymin = y0; 75 int ymax = y1+1; 76 77 // avoids to draw out of bounds 78 if (xmin < 0) 79 xmin = 0; 80 if (ymin < 0) 81 ymin = 0; 82 if (xmax > v.w) 83 xmax = v.w; 84 if (ymax > v.h) 85 ymax = v.h; 86 87 foreach (py; ymin .. ymax) 88 { 89 foreach (px; xmin .. xmax) 90 { 91 float dx = px-x; 92 float dy = py-y; 93 float rsq = dx * dx + dy * dy; 94 95 if(r0s <= rsq && rsq <= r1s) 96 { 97 float rs = sqrt(rsq); 98 99 // How much angle is one pixel at this radius? 100 // It's actually rule of 3. 101 // 2*pi radians => 2*pi*radius pixels 102 // ??? => 1 pixel 103 float aTransition = 1.0f / rs; 104 105 106 if (r0 <= rs && rs < r1) 107 { 108 float alpha = 1.0f; 109 if (r0 + 1 > rs) 110 alpha = rs - r0; 111 if (rs + 1 > r1) 112 alpha = r1 - rs; 113 114 float a = atan2(dy, dx); 115 bool inSector = (a0 <= a && a <= a1); 116 if (inSector) 117 { 118 float alpha2 = alpha; 119 if (a0 + aTransition > a) 120 alpha2 *= (a-a0) / aTransition; 121 else if (a + aTransition > a1) 122 alpha2 *= (a1 - a)/aTransition; 123 124 auto p = v.pixelPtr(px, py); 125 *p = COLOR.op!q{.blend(a, b, c)}(c, *p, cast(ChannelType)(0.5f + alpha2 * ChannelType.max)); 126 } 127 else 128 { 129 a += 2 * PI; 130 bool inSector2 = (a0 <= a && a <= a1); 131 if(inSector2 ) 132 { 133 float alpha2 = alpha; 134 if (a0 + aTransition > a) 135 alpha2 *= (a-a0) / aTransition; 136 else if (a + aTransition > a1) 137 alpha2 *= (a1 - a)/aTransition; 138 139 auto p = v.pixelPtr(px, py); 140 *p = COLOR.op!q{.blend(a, b, c)}(c, *p, cast(ChannelType)(0.5f + alpha2 * ChannelType.max)); 141 } 142 } 143 } 144 } 145 } 146 } 147 } 148 149 /// Fill rectangle while interpolating a color horiontally 150 void horizontalSlope(float curvature = 1.0f, V, COLOR)(auto ref V v, box2i rect, COLOR c0, COLOR c1) 151 if (isWritableView!V && is(COLOR : ViewColor!V)) 152 { 153 alias ChannelType = COLOR.ChannelType; 154 155 box2i inter = box2i(0, 0, v.w, v.h).intersection(rect); 156 157 int x0 = rect.min.x; 158 int x1 = rect.max.x; 159 immutable float invX1mX0 = 1.0f / (x1 - x0); 160 161 foreach (px; inter.min.x .. inter.max.x) 162 { 163 float fAlpha = (px - x0) * invX1mX0; 164 static if (curvature != 1.0f) 165 fAlpha = fAlpha ^^ curvature; 166 ChannelType alpha = cast(ChannelType)( 0.5f + ChannelType.max * fAlpha ); // Not being generic here 167 COLOR c = COLOR.op!q{.blend(a, b, c)}(c1, c0, alpha); // warning .blend is confusing, c1 comes first 168 vline(v, px, inter.min.y, inter.max.y, c); 169 } 170 } 171 172 void verticalSlope(float curvature = 1.0f, V, COLOR)(auto ref V v, box2i rect, COLOR c0, COLOR c1) 173 if (isWritableView!V && is(COLOR : ViewColor!V)) 174 { 175 alias ChannelType = COLOR.ChannelType; 176 177 box2i inter = box2i(0, 0, v.w, v.h).intersection(rect); 178 179 int x0 = rect.min.x; 180 int y0 = rect.min.y; 181 int x1 = rect.max.x; 182 int y1 = rect.max.y; 183 184 immutable float invY1mY0 = 1.0f / (y1 - y0); 185 186 foreach (py; inter.min.y .. inter.max.y) 187 { 188 float fAlpha = (py - y0) * invY1mY0; 189 static if (curvature != 1.0f) 190 fAlpha = fAlpha ^^ curvature; 191 ChannelType alpha = cast(ChannelType)( 0.5f + ChannelType.max * fAlpha ); // Not being generic here 192 COLOR c = COLOR.op!q{.blend(a, b, c)}(c1, c0, alpha); // warning .blend is confusing, c1 comes first 193 hline(v, inter.min.x, inter.max.x, py, c); 194 } 195 } 196 197 198 void aaSoftDisc(float curvature = 1.0f, V, COLOR)(auto ref V v, float x, float y, float r1, float r2, COLOR color, float globalAlpha = 1.0f) 199 if (isWritableView!V && is(COLOR : ViewColor!V)) 200 { 201 alias ChannelType = COLOR.ChannelType; 202 assert(r1 <= r2); 203 int x1 = cast(int)(x-r2-1); if (x1<0) x1=0; 204 int y1 = cast(int)(y-r2-1); if (y1<0) y1=0; 205 int x2 = cast(int)(x+r2+1); if (x2>v.w) x2 = v.w; 206 int y2 = cast(int)(y+r2+1); if (y2>v.h) y2 = v.h; 207 208 auto r1s = r1*r1; 209 auto r2s = r2*r2; 210 211 float fx = x; 212 float fy = y; 213 214 immutable float fr1s = r1s; 215 immutable float fr2s = r2s; 216 217 immutable float fr21 = fr2s - fr1s; 218 immutable float invfr21 = 1 / fr21; 219 220 for (int cy=y1;cy<y2;cy++) 221 { 222 auto row = v.scanline(cy); 223 for (int cx=x1;cx<x2;cx++) 224 { 225 float dx = (fx - cx); 226 float dy = (fy - cy); 227 float frs = dx*dx + dy*dy; 228 229 if (frs<fr1s) 230 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], cast(ChannelType)(0.5f + ChannelType.max * globalAlpha)); 231 else 232 { 233 if (frs<fr2s) 234 { 235 float alpha = (frs-fr1s) * invfr21; 236 static if (curvature != 1.0f) 237 alpha = alpha ^^ curvature; 238 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], cast(ChannelType)(0.5f + ChannelType.max * (1-alpha) * globalAlpha)); 239 } 240 } 241 } 242 } 243 } 244 245 void aaSoftEllipse(float curvature = 1.0f, V, COLOR)(auto ref V v, float x, float y, float r1, float r2, float scaleX, float scaleY, COLOR color, float globalAlpha = 1.0f) 246 if (isWritableView!V && is(COLOR : ViewColor!V)) 247 { 248 alias ChannelType = COLOR.ChannelType; 249 assert(r1 <= r2); 250 int x1 = cast(int)(x-r2*scaleX-1); if (x1<0) x1=0; 251 int y1 = cast(int)(y-r2*scaleY-1); if (y1<0) y1=0; 252 int x2 = cast(int)(x+r2*scaleX+1); if (x2>v.w) x2 = v.w; 253 int y2 = cast(int)(y+r2*scaleY+1); if (y2>v.h) y2 = v.h; 254 255 float invScaleX = 1 / scaleX; 256 float invScaleY = 1 / scaleY; 257 258 auto r1s = r1*r1; 259 auto r2s = r2*r2; 260 261 float fx = x; 262 float fy = y; 263 264 immutable float fr1s = r1s; 265 immutable float fr2s = r2s; 266 267 immutable float fr21 = fr2s - fr1s; 268 immutable float invfr21 = 1 / fr21; 269 270 for (int cy=y1;cy<y2;cy++) 271 { 272 auto row = v.scanline(cy); 273 for (int cx=x1;cx<x2;cx++) 274 { 275 float dx = (fx - cx) * invScaleX; 276 float dy = (fy - cy) * invScaleY; 277 float frs = dx*dx + dy*dy; 278 279 if (frs<fr1s) 280 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], cast(ChannelType)(0.5f + ChannelType.max * globalAlpha)); 281 else 282 { 283 if (frs<fr2s) 284 { 285 float alpha = (frs-fr1s) * invfr21; 286 static if (curvature != 1.0f) 287 alpha = alpha ^^ curvature; 288 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], cast(ChannelType)(0.5f + ChannelType.max * (1-alpha) * globalAlpha)); 289 } 290 } 291 } 292 } 293 } 294 295 /// Draw a circle gradually fading in between r1 and r2 and fading out between r2 and r3 296 void aaSoftCircle(float curvature = 1.0f, V, COLOR)(auto ref V v, float x, float y, float r1, float r2, float r3, COLOR color, float globalAlpha = 1.0f) 297 if (isWritableView!V && is(COLOR : ViewColor!V)) 298 { 299 alias ChannelType = COLOR.ChannelType; 300 assert(r1 <= r2); 301 assert(r2 <= r3); 302 int x1 = cast(int)(x-r3-1); if (x1<0) x1=0; 303 int y1 = cast(int)(y-r3-1); if (y1<0) y1=0; 304 int x2 = cast(int)(x+r3+1); if (x2>v.w) x2 = v.w; 305 int y2 = cast(int)(y+r3+1); if (y2>v.h) y2 = v.h; 306 307 auto r1s = r1*r1; 308 auto r2s = r2*r2; 309 auto r3s = r3*r3; 310 311 float fx = x; 312 float fy = y; 313 314 immutable float fr1s = r1s; 315 immutable float fr2s = r2s; 316 immutable float fr3s = r3s; 317 318 immutable float fr21 = fr2s - fr1s; 319 immutable float fr32 = fr3s - fr2s; 320 immutable float invfr21 = 1 / fr21; 321 immutable float invfr32 = 1 / fr32; 322 323 for (int cy=y1;cy<y2;cy++) 324 { 325 auto row = v.scanline(cy); 326 for (int cx=x1;cx<x2;cx++) 327 { 328 float frs = (fx - cx)*(fx - cx) + (fy - cy)*(fy - cy); 329 330 if (frs >= fr1s) 331 { 332 if (frs < fr3s) 333 { 334 float alpha = void; 335 if (frs >= fr2s) 336 alpha = (frs - fr2s) * invfr32; 337 else 338 alpha = 1 - (frs - fr1s) * invfr21; 339 340 static if (curvature != 1.0f) 341 alpha = alpha ^^ curvature; 342 row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], cast(ChannelType)(0.5f + ChannelType.max * (1-alpha) * globalAlpha)); 343 } 344 } 345 } 346 } 347 } 348 349 350 void aaFillRectFloat(bool CHECKED=true, V, COLOR)(auto ref V v, float x1, float y1, float x2, float y2, COLOR color, float globalAlpha = 1.0f) 351 if (isWritableView!V && is(COLOR : ViewColor!V)) 352 { 353 if (globalAlpha == 0) 354 return; 355 356 alias ChannelType = COLOR.ChannelType; 357 358 sort2(x1, x2); 359 sort2(y1, y2); 360 361 int ix1 = cast(int)(floor(x1)); 362 int iy1 = cast(int)(floor(y1)); 363 int ix2 = cast(int)(floor(x2)); 364 int iy2 = cast(int)(floor(y2)); 365 float fx1 = x1 - ix1; 366 float fy1 = y1 - iy1; 367 float fx2 = x2 - ix2; 368 float fy2 = y2 - iy2; 369 370 static ChannelType toAlpha(float fraction) pure nothrow @nogc 371 { 372 return cast(ChannelType)(cast(int)(0.5f + ChannelType.max * fraction)); 373 } 374 375 v.aaPutPixelFloat!CHECKED(ix1, iy1, color, toAlpha(globalAlpha * (1-fx1) * (1-fy1) )); 376 v.hline!CHECKED(ix1+1, ix2, iy1, color, toAlpha(globalAlpha * (1 - fy1) )); 377 v.aaPutPixelFloat!CHECKED(ix2, iy1, color, toAlpha(globalAlpha * fx2 * (1-fy1) )); 378 379 v.vline!CHECKED(ix1, iy1+1, iy2, color, toAlpha(globalAlpha * (1 - fx1))); 380 v.vline!CHECKED(ix2, iy1+1, iy2, color, toAlpha(globalAlpha * fx2)); 381 382 v.aaPutPixelFloat!CHECKED(ix1, iy2, color, toAlpha(globalAlpha * (1-fx1) * fy2 )); 383 v.hline!CHECKED(ix1+1, ix2, iy2, color, toAlpha(globalAlpha * fy2)); 384 v.aaPutPixelFloat!CHECKED(ix2, iy2, color, toAlpha(globalAlpha * fx2 * fy2 )); 385 386 v.fillRectFloat!CHECKED(ix1+1, iy1+1, ix2, iy2, color, globalAlpha); 387 } 388 389 void fillRectFloat(bool CHECKED=true, V, COLOR)(auto ref V v, int x1, int y1, int x2, int y2, COLOR b, float globalAlpha = 1.0f) // [) 390 if (isWritableView!V && is(COLOR : ViewColor!V)) 391 { 392 if (globalAlpha == 0) 393 return; 394 395 sort2(x1, x2); 396 sort2(y1, y2); 397 static if (CHECKED) 398 { 399 if (x1 >= v.w || y1 >= v.h || x2 <= 0 || y2 <= 0 || x1==x2 || y1==y2) return; 400 if (x1 < 0) x1 = 0; 401 if (y1 < 0) y1 = 0; 402 if (x2 >= v.w) x2 = v.w; 403 if (y2 >= v.h) y2 = v.h; 404 } 405 406 if (globalAlpha == 1) 407 { 408 foreach (y; y1..y2) 409 v.scanline(y)[x1..x2] = b; 410 } 411 else 412 { 413 alias ChannelType = COLOR.ChannelType; 414 static ChannelType toAlpha(float fraction) pure nothrow @nogc 415 { 416 return cast(ChannelType)(cast(int)(0.5f + ChannelType.max * fraction)); 417 } 418 419 ChannelType alpha = toAlpha(globalAlpha); 420 421 foreach (y; y1..y2) 422 { 423 COLOR[] scan = v.scanline(y); 424 foreach (x; x1..x2) 425 { 426 scan[x] = COLOR.op!q{.blend(a, b, c)}(b, scan[x], alpha); 427 } 428 } 429 } 430 } 431 432 void aaPutPixelFloat(bool CHECKED=true, V, COLOR, A)(auto ref V v, int x, int y, COLOR color, A alpha) 433 if (is(COLOR.ChannelType == A)) 434 { 435 static if (CHECKED) 436 if (x<0 || x>=v.w || y<0 || y>=v.h) 437 return; 438 439 COLOR* p = v.pixelPtr(x, y); 440 *p = COLOR.op!q{.blend(a, b, c)}(color, *p, alpha); 441 } 442 443 444 /// Blits a view onto another. 445 /// The views must have the same size. 446 /// PERF: optimize that 447 void blendWithAlpha(SRC, DST)(auto ref SRC srcView, auto ref DST dstView, auto ref ImageRef!L8 alphaView) 448 { 449 static assert(isDirectView!SRC); 450 static assert(isDirectView!DST); 451 static assert(isWritableView!DST); 452 453 static ubyte blendByte(ubyte a, ubyte b, ubyte f) nothrow @nogc 454 { 455 int sum = ( f * a + b * (~f) ) + 127; 456 return cast(ubyte)(sum / 255 );// ((sum+1)*257) >> 16 ); // integer divide by 255 457 } 458 459 static ushort blendShort(ushort a, ushort b, ubyte f) nothrow @nogc 460 { 461 ushort ff = (f << 8) | f; 462 int sum = ( ff * a + b * (~ff) ) + 32768; 463 return cast(ushort)( sum >> 16 ); // MAYDO: this doesn't map to the full range 464 } 465 466 alias COLOR = ViewColor!DST; 467 assert(srcView.w == dstView.w && srcView.h == dstView.h, "View size mismatch"); 468 469 foreach (y; 0..srcView.h) 470 { 471 COLOR* srcScan = srcView.scanline(y).ptr; 472 COLOR* dstScan = dstView.scanline(y).ptr; 473 L8* alphaScan = alphaView.scanline(y).ptr; 474 475 foreach (x; 0..srcView.w) 476 { 477 ubyte alpha = alphaScan[x].l; 478 if (alpha == 0) 479 continue; 480 static if (is(COLOR == RGBA)) 481 { 482 dstScan[x].r = blendByte(srcScan[x].r, dstScan[x].r, alpha); 483 dstScan[x].g = blendByte(srcScan[x].g, dstScan[x].g, alpha); 484 dstScan[x].b = blendByte(srcScan[x].b, dstScan[x].b, alpha); 485 dstScan[x].a = blendByte(srcScan[x].a, dstScan[x].a, alpha); 486 } 487 else static if (is(COLOR == L16)) 488 dstScan[x].l = blendShort(srcScan[x].l, dstScan[x].l, alpha); 489 else 490 static assert(false); 491 } 492 } 493 } 494 495 496 /// Manually managed image which is also GC-proof. 497 class OwnedImage(COLOR) 498 { 499 public: 500 nothrow: 501 @nogc: 502 int w, h; 503 504 /// Create empty. 505 this() nothrow @nogc 506 { 507 w = 0; 508 h = 0; 509 _pixels = null; 510 } 511 512 /// Create with given initial size. 513 this(int w, int h) nothrow @nogc 514 { 515 this(); 516 size(w, h); 517 } 518 519 ~this() 520 { 521 if (_pixels !is null) 522 { 523 alignedFree(_pixels, 128); 524 _pixels = null; 525 } 526 } 527 528 /// Returns an array for the pixels at row y. 529 COLOR[] scanline(int y) pure nothrow @nogc 530 { 531 assert(y>=0 && y<h); 532 auto start = w*y; 533 return _pixels[start..start+w]; 534 } 535 536 mixin DirectView; 537 538 /// Resize the image, the content is lost. 539 void size(int w, int h) nothrow @nogc 540 { 541 this.w = w; 542 this.h = h; 543 size_t sizeInBytes = w * h * COLOR.sizeof; 544 _pixels = cast(COLOR*) alignedRealloc(_pixels, sizeInBytes, 128); 545 } 546 547 /// Returns: A slice of all pixels. 548 COLOR[] pixels() nothrow @nogc 549 { 550 return _pixels[0..w*h]; 551 } 552 553 private: 554 COLOR* _pixels; 555 } 556 557 unittest 558 { 559 static assert(isDirectView!(OwnedImage!ubyte)); 560 } 561 562 // 563 // Image loading 564 // 565 struct IFImage 566 { 567 int w, h; 568 ubyte[] pixels; 569 int channels; // number of channels 570 571 void free() nothrow @nogc 572 { 573 if (pixels.ptr !is null) 574 .free(pixels.ptr); 575 } 576 } 577 578 IFImage readImageFromMem(const(ubyte[]) imageData, int channels) 579 { 580 static immutable ubyte[8] pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 581 bool isPNG = imageData.length >= 8 && (imageData[0..8] == pngSignature); 582 583 // PNG are decoded using stb_image to avoid GC overload using zlib 584 if (isPNG) 585 { 586 int width, height, components; 587 ubyte* decoded = stbi_load_png_from_memory(imageData, width, height, components, channels); 588 IFImage result; 589 result.w = width; 590 result.h = height; 591 result.channels = channels; 592 int size = width * height * channels; 593 result.pixels = decoded[0..size]; 594 return result; 595 } 596 else 597 { 598 bool isJPEG = (imageData.length >= 2) && (imageData[0] == 0xff) && (imageData[1] == 0xd8); 599 600 if (isJPEG) 601 { 602 import dplug.graphics.jpegload; 603 IFImage result; 604 int comp; 605 ubyte[] pixels = decompress_jpeg_image_from_memory(imageData, result.w, result.h, comp, channels); 606 result.channels = channels; 607 result.pixels = pixels; 608 return result; 609 } 610 else 611 assert(false); // Only PNG and JPEG are supported 612 } 613 } 614 615 /// The one function you probably want to use. 616 /// Loads an image from a static array. 617 /// The OwnedImage is allocated with `mallocEmplace` and should be destroyed with `destroyFree`. 618 /// Throws: $(D ImageIOException) on error. 619 OwnedImage!RGBA loadOwnedImage(in void[] imageData) 620 { 621 IFImage ifImage = readImageFromMem(cast(const(ubyte[])) imageData, 4); 622 scope(exit) ifImage.free(); 623 int width = cast(int)ifImage.w; 624 int height = cast(int)ifImage.h; 625 626 OwnedImage!RGBA loaded = mallocEmplace!(OwnedImage!RGBA)(width, height); 627 loaded.pixels[] = (cast(RGBA[]) ifImage.pixels)[]; // pixel copy here 628 return loaded; 629 } 630 631 632 633 /// Loads two different images: 634 /// - the 1st is the RGB channels 635 /// - the 2nd is interpreted as greyscale and fetch in the alpha channel of the result. 636 /// The OwnedImage is allocated with `mallocEmplace` and should be destroyed with `destroyFree`. 637 /// Throws: $(D ImageIOException) on error. 638 OwnedImage!RGBA loadImageSeparateAlpha(in void[] imageDataRGB, in void[] imageDataAlpha) 639 { 640 IFImage ifImageRGB = readImageFromMem(cast(const(ubyte[])) imageDataRGB, 3); 641 scope(exit) ifImageRGB.free(); 642 int widthRGB = cast(int)ifImageRGB.w; 643 int heightRGB = cast(int)ifImageRGB.h; 644 645 IFImage ifImageA = readImageFromMem(cast(const(ubyte[])) imageDataAlpha, 1); 646 scope(exit) ifImageA.free(); 647 int widthA = cast(int)ifImageA.w; 648 int heightA = cast(int)ifImageA.h; 649 650 if ( (widthA != widthRGB) || (heightRGB != heightA) ) 651 assert(false, "Image size mismatch"); 652 653 int width = widthA; 654 int height = heightA; 655 656 OwnedImage!RGBA loaded = mallocEmplace!(OwnedImage!RGBA)(width, height); 657 658 for (int j = 0; j < height; ++j) 659 { 660 RGB* rgbscan = cast(RGB*)(&ifImageRGB.pixels[3 * (j * width)]); 661 ubyte* ascan = &ifImageA.pixels[j * width]; 662 RGBA[] outscan = loaded.scanline(j); 663 for (int i = 0; i < width; ++i) 664 { 665 RGB rgb = rgbscan[i]; 666 outscan[i] = RGBA(rgb.r, rgb.g, rgb.b, ascan[i]); 667 } 668 } 669 return loaded; 670 } 671