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