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