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