1 /**
2 In-memory images.
3 
4 License:
5     This Source Code Form is subject to the terms of
6     the Mozilla Public License, v. 2.0. If a copy of
7     the MPL was not distributed with this file, You
8     can obtain one at http://mozilla.org/MPL/2.0/.
9  
10  Copyright: Vladimir Panteleev <vladimir@thecybershadow.net>
11  Copyright: Guillaume Piolat <contact@auburnsounds.com>
12  */
13 
14 module dplug.graphics.image;
15 
16 
17 import std.conv : to;
18 import std.string : format;
19 import std.functional;
20 import std.typetuple;
21 import std.algorithm.mutation: swap;
22 import std.math;
23 import dplug.core.math;
24 import dplug.core.vec;
25 import dplug.core.nogc;
26 import dplug.math.box;
27 public import dplug.graphics.color;
28 import gamut;
29 
30 nothrow @nogc:
31 
32 /// A view is any type which provides a width, height,
33 /// and can be indexed to get the color at a specific
34 /// coordinate.
35 enum isView(T) =
36     is(typeof(T.init.w) : size_t) && // width
37     is(typeof(T.init.h) : size_t) && // height
38     is(typeof(T.init[0, 0])     );   // color information
39 
40 /// Returns the color type of the specified view.
41 /// By convention, colors are structs with numeric
42 /// fields named after the channel they indicate.
43 alias ViewColor(T) = typeof(T.init[0, 0]);
44 
45 /// Views can be read-only or writable.
46 enum isWritableView(T) =
47     isView!T &&
48     is(typeof(T.init[0, 0] = ViewColor!T.init));
49 
50 /// Optionally, a view can also provide direct pixel
51 /// access. We call these "direct views".
52 enum isDirectView(T) =
53     isView!T &&
54     is(typeof(T.init.scanline(0)) : ViewColor!T[]);
55 
56 /// Mixin which implements view primitives on top of
57 /// existing direct view primitives.
58 mixin template DirectView()
59 {
60     alias COLOR = typeof(scanline(0)[0]);
61 
62     /// Implements the view[x, y] operator.
63     ref COLOR opIndex(int x, int y)
64     {
65         return scanline(y)[x];
66     }
67 
68     /// Implements the view[x, y] = c operator.
69     COLOR opIndexAssign(COLOR value, int x, int y)
70     {
71         return scanline(y)[x] = value;
72     }
73 }
74 
75 /// Blits a view onto another.
76 /// The views must have the same size.
77 void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst)
78 	if (isView!SRC && isWritableView!DST)
79 {
80 	assert(src.w == dst.w && src.h == dst.h, "View size mismatch");
81 	foreach (y; 0..src.h)
82 	{
83 		static if (isDirectView!SRC && isDirectView!DST)
84 			dst.scanline(y)[] = src.scanline(y)[];
85 		else
86 		{
87 			foreach (x; 0..src.w)
88 				dst[x, y] = src[x, y];
89 		}
90 	}
91 }
92 
93 /// Helper function to blit an image onto another at a specified location.
94 void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst, int x, int y)
95 {
96 	src.blitTo(dst.crop(x, y, x+src.w, y+src.h));
97 }
98 
99 /// Default implementation for the .size method.
100 /// Asserts that the view has the desired size.
101 void size(V)(auto ref V src, int w, int h)
102 	if (isView!V)
103 {
104 	assert(src.w == w && src.h == h, "Wrong size for " ~ V.stringof);
105 }
106 
107 // ***************************************************************************
108 
109 /// Mixin which implements view primitives on top of
110 /// another view, using a coordinate transform function.
111 mixin template Warp(V)
112 	if (isView!V)
113 {
114 	V src;
115 
116 	auto ref ViewColor!V opIndex(int x, int y)
117 	{
118 		warp(x, y);
119 		return src[x, y];
120 	}
121 
122 	static if (isWritableView!V)
123 	ViewColor!V opIndexAssign(ViewColor!V value, int x, int y)
124 	{
125 		warp(x, y);
126 		return src[x, y] = value;
127 	}
128 }
129 
130 /// Represents a reference to COLOR data
131 /// already existing elsewhere in memory.
132 /// Assumes that pixels are stored row-by-row,
133 /// with a known distance between each row.
134 struct ImageRef(COLOR)
135 {
136 	int w, h;
137 	size_t pitch; /// In bytes, not COLORs
138 	COLOR* pixels;
139 
140 	/// Returns an array for the pixels at row y.
141 	COLOR[] scanline(int y)
142 	{
143 		assert(y >= 0 && y < h);
144 		assert(pitch);
145 
146         // perf: this cast help in 64-bit, since it avoids a MOVSXD to extend a signed 32-bit into a 64-bit pointer offset
147         uint unsignedY = cast(uint)y;
148 
149 		auto row = cast(COLOR*)(cast(ubyte*)pixels + unsignedY * pitch);
150 		return row[0..w];
151 	}
152 
153 	mixin DirectView;
154 
155     /// Returns a cropped view of the same `ImageRef`.
156     ImageRef!COLOR cropBorder(int borderPixels)
157     {
158         assert(w >= 2*borderPixels);
159         assert(h >= 2*borderPixels);
160 
161         ImageRef cropped;
162         cropped.w = w - 2*borderPixels;
163         cropped.h = h - 2*borderPixels;
164         cropped.pitch = pitch;
165         cropped.pixels = cast(COLOR*)(cast(ubyte*)pixels + borderPixels*pitch + borderPixels*COLOR.sizeof);
166         return cropped;
167     }
168 }
169 
170 unittest
171 {
172     static assert(isDirectView!(ImageRef!ubyte));
173 }
174 
175 /// Convert an `OwnedImage` to an `ImageRef`.
176 ImageRef!L8 toRef(OwnedImage!L8 src) pure
177 {
178     return ImageRef!L8(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0));
179 }
180 
181 ///ditto
182 ImageRef!L16 toRef(OwnedImage!L16 src) pure
183 {
184     return ImageRef!L16(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0));
185 }
186 
187 ///ditto
188 ImageRef!RGBA toRef(OwnedImage!RGBA src) pure
189 {
190     return ImageRef!RGBA(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0));
191 }
192 
193 /// Copy the indicated row of src to a COLOR buffer.
194 void copyScanline(SRC, COLOR)(auto ref SRC src, int y, COLOR[] dst)
195     if (isView!SRC && is(COLOR == ViewColor!SRC))
196 {
197 	static if (isDirectView!SRC)
198 		dst[] = src.scanline(y)[];
199 	else
200 	{
201 		assert(src.w == dst.length);
202 		foreach (x; 0..src.w)
203 			dst[x] = src[x, y];
204 	}
205 }
206 
207 /// Copy a view's pixels (top-to-bottom) to a COLOR buffer.
208 void copyPixels(SRC, COLOR)(auto ref SRC src, COLOR[] dst)
209 	if (isView!SRC && is(COLOR == ViewColor!SRC))
210 {
211 	assert(dst.length == src.w * src.h);
212 	foreach (y; 0..src.h)
213 		src.copyScanline(y, dst[y*src.w..(y+1)*src.w]);
214 }
215 
216 // ***************************************************************************
217 
218 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12433
219 
220 struct InputColor {}
221 alias GetInputColor(COLOR, INPUT) = Select!(is(COLOR == InputColor), INPUT, COLOR);
222 
223 struct TargetColor {}
224 enum isTargetColor(C, TARGET) = is(C == TargetColor) || is(C == ViewColor!TARGET);
225 
226 // ***************************************************************************
227 
228 
229 /// Crop an ImageRef and get an ImageRef instead of a Voldemort type.
230 /// This also avoid adding offset to coordinates.
231 ImageRef!COLOR cropImageRef(COLOR)(ImageRef!COLOR src, box2i rect)
232 {
233     assert(rect.max.x <= src.w);
234     assert(rect.max.y <= src.h);
235     ImageRef!COLOR result;
236     result.w = rect.width;
237     result.h = rect.height;
238     result.pitch = src.pitch;
239     COLOR[] scan = src.scanline(rect.min.y);
240     result.pixels = &scan[rect.min.x];
241     return result;
242 }
243 ///ditto
244 ImageRef!COLOR cropImageRef(COLOR)(ImageRef!COLOR src, int xmin, int ymin, int xmax, int ymax)
245 {
246     return src.cropImageRef(box2i(xmin, ymin, xmax, ymax));
247 }
248 
249 /// Manually managed image.
250 final class OwnedImage(COLOR)
251 {
252 public:
253 nothrow:
254 @nogc:
255 
256     /// Width of the meaningful area. Public in order to be an `Image`.
257     int w;
258 
259     /// Height of the meaningful area. Public in order to be an `Image`.
260     int h;
261 
262     /// Create empty and with no size.
263     this() nothrow @nogc
264     {
265     }
266 
267     /// Create with given initial size.
268     ///
269     /// Params:
270     ///   w               Width of meaningful area in pixels.
271     ///   h               Height of meaningful area in pixels.
272     ///   border          Number of border pixels around the meaningful area. For the right border, this could actually be more depending on other constraints.
273     ///   rowAlignment    Alignment of the _first_ pixel of each row, in bytes (excluding border).
274     ///   xMultiplicity   Starting with the first meaningful pixel of a line, force the number of adressable
275     ///                   pixels to be a multiple of `xMultiplicity`.
276     ///                   All these "padding" samples added at the right of each line if needed will be considered 
277     ///                   part of the border and replicated if need be.
278     ///                   This is eg. to process a whole line with only aligned loads.
279     ///                   Implemented with additional right border.
280     ///   trailingSamples When reading from a meaningful position, you can read 1 + `trailingSamples` neightbours
281     ///                   Their value is NOT guaranteed in any way and is completely undefined. 
282     ///                   They are guaranteed non-NaN if you've generated borders and did not write into borders.
283     ///                   Because of row alignment padding, this is not implemented with a few samples at the end 
284     ///                   of the buffer, but is instead again a right border extension.
285     ///
286     /// Note: default arguments leads to a gapless representation.
287     this(int w, int h, int border = 0, int rowAlignment = 1, int xMultiplicity = 1, int trailingSamples = 0) nothrow @nogc
288     {
289         this();
290         size(w, h, border, rowAlignment, xMultiplicity, trailingSamples);
291     }
292 
293     /// Create from already loaded dense RGBA pixel data.
294     /// `buffer` should be allocated with `alignedMalloc`/`alignedRealloc` with an alignment of 1.
295     /// Ownership of `buffer` is given to the `OwnedImage`.
296     this(int w, int h, ubyte* buffer)
297     {
298         this.w = w;
299         this.h = h;
300         _pixels = cast(COLOR*) buffer;
301         _bytePitch = w * cast(int)(COLOR.sizeof);
302         _buffer = buffer;
303         _border = 0;
304         _borderRight = 0;
305     }
306 
307     ~this()
308     {
309         if (_buffer !is null)
310         {
311              // FUTURE: use intel-intrinsics _mm_malloc/_mm_free
312             alignedFree(_buffer, 1); // Note: this allocation methods must be synced with the one in JPEG and PNL loading.
313             _buffer = null;
314         }
315     }
316 
317     /// Returns: A pointer to the pixels at row y. Excluding border pixels.
318     COLOR* scanlinePtr(int y) pure
319     {
320         assert(y >= 0 && y < h);
321 
322         // perf: this cast help in 64-bit, since it avoids a MOVSXD to extend a signed 32-bit into a 64-bit pointer offset
323         uint offsetBytes = _bytePitch * cast(uint)y;
324 
325         return cast(COLOR*)( cast(ubyte*)_pixels + offsetBytes );
326     }
327 
328     /// Returns: A slice of the pixels at row y. Excluding border pixels.
329     COLOR[] scanline(int y) pure
330     {
331         return scanlinePtr(y)[0..w];
332     }    
333 
334     /// Resize the image, the content is lost and the new content is undefined.
335     ///
336     /// Params:
337     ///   w               Width of meaningful area in pixels.
338     ///   h               Height of meaningful area in pixels.
339     ///   border          Number of border pixels around the meaningful area. For the right border, this could actually be more depending on other constraints.
340     ///   rowAlignment    Alignment of the _first_ pixel of each row, in bytes (excluding border).
341     ///   xMultiplicity   Starting with the first meaningful pixel of a line, force the number of adressable
342     ///                   pixels to be a multiple of `xMultiplicity`.
343     ///                   All these "padding" samples added at the right of each line if needed will be considered 
344     ///                   part of the border and replicated if need be.
345     ///                   This is eg. to process a whole line with only aligned loads.
346     ///                   Implemented with additional right border.
347     ///   trailingSamples When reading from a meaningful position, you can read 1 + `trailingSamples` neightbours
348     ///                   Their value is NOT guaranteed in any way and is completely undefined. 
349     ///                   They are guaranteed non-NaN if you've generated borders and did not write into borders.
350     ///                   Because of row alignment padding, this is not implemented with a few samples at the end 
351     ///                   of the buffer, but is instead again a right border extension.
352     ///
353     /// Note: Default arguments leads to a gapless representation.
354     void size(int width, 
355               int height, 
356               int border = 0, 
357               int rowAlignment = 1, 
358               int xMultiplicity = 1, 
359               int trailingSamples = 0)
360     {
361         assert(width >= 0); // Note sure if 0 is supported
362         assert(height >= 0); // Note sure if 0 is supported
363         assert(border >= 0);
364         assert(rowAlignment >= 1); // Not yet implemented!
365         assert(xMultiplicity >= 1); // Not yet implemented!
366 
367         // Compute size of right border.
368         // How many "padding samples" do we need to extend the right border with to respect `xMultiplicity`?
369         int rightPadding = computeRightPadding(w, border, xMultiplicity);
370         int borderRight = border + rightPadding;
371         if (borderRight < trailingSamples)
372             borderRight = trailingSamples;
373 
374         int actualWidthInSamples  = border + width  + borderRight;
375         int actualHeightInSamples = border + height + border;        
376 
377         // Compute byte pitch and align it on `rowAlignment`
378         int bytePitch = cast(int)(COLOR.sizeof) * actualWidthInSamples;
379         bytePitch = cast(int) nextMultipleOf(bytePitch, rowAlignment);
380 
381         this.w = width;
382         this.h = height;
383         this._border = border;
384         this._borderRight = borderRight;
385         this._bytePitch = bytePitch;
386 
387         // How many bytes do we need for all samples? A bit more for aligning the first valid pixel.
388         size_t allocationSize = bytePitch * actualHeightInSamples;
389         allocationSize += (rowAlignment - 1);
390 
391         // We don't need to preserve former data, nor to align the first border pixel
392         this._buffer = alignedReallocDiscard(this._buffer, allocationSize, 1);
393 
394         // Set _pixels to the right place
395         size_t offsetToFirstMeaningfulPixel = bytePitch * borderTop() + COLOR.sizeof * borderLeft();
396         this._pixels = cast(COLOR*) nextAlignedPointer(&_buffer[offsetToFirstMeaningfulPixel], rowAlignment);
397 
398         // Test alignment of rows
399         assert( isPointerAligned(_pixels, rowAlignment) );
400         if (height > 0)
401             assert( isPointerAligned(scanlinePtr(0), rowAlignment) );
402 
403         if (border == 0 && rowAlignment == 1 && xMultiplicity == 1 && trailingSamples == 0)
404         {
405             assert(isGapless());
406         }
407     }
408 
409     /// Returns: `true` if rows of pixels are immediately consecutive in memory.
410     ///          Meaning that there is no border or lost pixels in the data.
411     bool isGapless() pure
412     {
413         return w * COLOR.sizeof  == _bytePitch;
414     }
415 
416     /// Returns: Number of bytes to add to a COLOR* pointer to get to the previous/next line.
417     ///          This pitch is guaranteed to be positive (>= 0).
418     int pitchInBytes() pure
419     {
420         return _bytePitch;
421     }
422 
423     /// Returns: Number of border pixels in the left direction (small X).
424     int borderLeft() pure
425     {
426         return _border;
427     }
428 
429     /// Returns: Number of border pixels in the right direction (large X).
430     int borderRight() pure
431     {
432         return _borderRight;
433     }
434 
435     /// Returns: Number of border pixels in the left direction (small Y).
436     int borderTop() pure
437     {
438         return _border;
439     }
440 
441     /// Returns: Number of border pixels in the left direction (large Y).
442     int borderBottom() pure
443     {
444         return _border;
445     }
446 
447     // It is a `DirectView`.
448     mixin DirectView;
449 
450     /// Fills the whole image, border included, with a single color value.
451     void fillWith(COLOR fill)
452     {
453         for (int y = -borderTop(); y < h + borderBottom(); ++y)
454         {
455             int pixelsInRow = borderLeft() + w + borderRight();
456             COLOR* scan = unsafeScanlinePtr(y) - borderLeft();
457             scan[0..pixelsInRow] = fill;
458         }
459     }
460 
461     /// Fill the borders by taking the nearest existing pixel in the meaningful area.
462     void replicateBorders()
463     {
464         replicateBordersTouching( box2i.rectangle(0, 0, this.w, this.h) );
465     }
466 
467     /// Fill the borders _touching updatedRect_ by taking the nearest existing pixel in the meaningful area.
468     void replicateBordersTouching(box2i updatedRect)
469     {
470         if (w < 1 || h < 1)
471             return; // can't replicate borders of an empty image
472 
473         // BORDER REPLICATION.
474         // If an area is touching left border, then the border needs replication.
475         // If an area is touching left and top border, then the border needs replication and the corner pixel should be filled too.
476         // Same for all four corners. Border only applies to level 0.
477         //
478         // OOOOOOOxxxxxxxxxxxxxxx
479         // Ooooooo
480         // Oo    o
481         // Oo    o    <------ if the update area is made of 'o', then the area to replicate is 'O'
482         // Ooooooo
483         // x
484         // x
485 
486         int W = this.w;
487         int H = this.h;
488 
489         int minx = updatedRect.min.x;
490         int maxx = updatedRect.max.x;
491         int miny = updatedRect.min.y;
492         int maxy = updatedRect.max.y;
493 
494         bool touchLeft   = (minx == 0);
495         bool touchTop    = (miny == 0);
496         bool touchRight  = (maxx == W);
497         bool touchBottom = (maxy == H);
498 
499         int bTop   = borderTop();
500         int bBott  = borderBottom();
501         int bLeft  = borderLeft();
502         int bRight = borderRight();
503 
504         if (touchTop)
505         {
506             if (touchLeft)
507             {
508                 COLOR topLeft = this[0, 0];
509                 for (int y = -borderTop; y < 0; ++y)
510                     for (int x = -borderLeft; x < 0; ++x)
511                         unsafeScanlinePtr(y)[x] = topLeft;
512             }
513 
514             for (int y = -borderTop; y < 0; ++y)
515             {
516                 unsafeScanlinePtr(y)[minx..maxx] = scanline(0)[minx..maxx];
517             }
518 
519             if (touchRight)
520             {
521                 COLOR topRight = this[W-1, 0];
522                 for (int y = -borderTop; y < 0; ++y)
523                     for (int x = 0; x < borderRight(); ++x)
524                         unsafeScanlinePtr(y)[W + x] = topRight;
525             }
526         }
527 
528         if (touchLeft)
529         {
530             for (int y = miny; y < maxy; ++y)
531             {
532                 COLOR edge = this[0, y];
533                 for (int x = -borderLeft(); x < 0; ++x)
534                     unsafeScanlinePtr(y)[x] = edge;
535             }
536         }
537 
538         if (touchRight)
539         {
540             for (int y = miny; y < maxy; ++y)
541             {
542                 COLOR edge = this[W-1, y];
543                 for (int x = 0; x < borderRight(); ++x)
544                     unsafeScanlinePtr(y)[W + x] = edge;
545             }
546         }
547 
548         if (touchBottom)
549         {
550             if (touchLeft)
551             {
552                 COLOR bottomLeft = this[0, H-1];
553                 for (int y = H; y < H + borderTop(); ++y)
554                     for (int x = -borderLeft; x < 0; ++x)
555                         unsafeScanlinePtr(y)[x] = bottomLeft;
556             }
557 
558             for (int y = H; y < H + borderBottom(); ++y)
559             {
560                 unsafeScanlinePtr(y)[minx..maxx] = scanline(H-1)[minx..maxx];
561             }
562 
563             if (touchRight)
564             {
565                 COLOR bottomRight = this[W-1, H-1];
566                 for (int y = H; y < H + borderTop(); ++y)
567                     for (int x = 0; x < borderRight(); ++x)
568                         unsafeScanlinePtr(y)[W + x] = bottomRight;
569             }
570         }
571     }
572 
573 private:
574 
575     /// Adress of the first meaningful pixel
576     COLOR* _pixels;
577 
578     /// Samples difference between rows of pixels.
579     int _bytePitch;
580 
581     /// Address of the allocation itself
582     void* _buffer = null;
583 
584     /// Size of left, top and bottom borders, around the meaningful area.
585     int _border;
586 
587     /// Size of border at thr right of the meaningful area (most positive X)
588     int _borderRight;
589 
590     // Internal use: allows to get the scanlines of top and bottom borders too
591     COLOR* unsafeScanlinePtr(int y) pure
592     {
593         assert(y >= -borderTop());      // negative Y allows to get top border scanlines
594         assert(y < h + borderBottom()); // Y overflow allows to get bottom border scanlines
595         int byteOffset = _bytePitch * y; // unlike the normal `scanlinePtr`, there will be a MOVSXD here
596         return cast(COLOR*)(cast(ubyte*)(_pixels) + byteOffset);
597     }
598 
599     static int computeRightPadding(int width, int border, int xMultiplicity) pure
600     {
601         int nextMultiple = cast(int)(nextMultipleOf(width + border, xMultiplicity));
602         return nextMultiple - (width + border);
603     }
604 
605     static size_t nextMultipleOf(size_t base, size_t multiple) pure
606     {
607         assert(multiple > 0);
608         size_t n = (base + multiple - 1) / multiple;
609         return multiple * n;
610     }
611 
612     /// Returns: next pointer aligned with alignment bytes.
613     static void* nextAlignedPointer(void* start, size_t alignment) pure
614     {
615         return cast(void*)nextMultipleOf(cast(size_t)(start), alignment);
616     }
617 }
618 
619 unittest
620 {
621     static assert(isDirectView!(OwnedImage!ubyte));
622     assert(OwnedImage!RGBA.computeRightPadding(4, 0, 4) == 0);
623     assert(OwnedImage!RGBA.computeRightPadding(1, 3, 5) == 1);
624     assert(OwnedImage!L16.computeRightPadding(2, 0, 4) == 2);
625     assert(OwnedImage!RGBA.computeRightPadding(2, 1, 4) == 1);
626     assert(OwnedImage!RGBf.nextMultipleOf(0, 7) == 0);
627     assert(OwnedImage!RGBAf.nextMultipleOf(1, 7) == 7);
628     assert(OwnedImage!RGBA.nextMultipleOf(6, 7) == 7);
629     assert(OwnedImage!RGBA.nextMultipleOf(7, 7) == 7);
630     assert(OwnedImage!RGBA.nextMultipleOf(8, 7) == 14);
631 
632     {
633         OwnedImage!RGBA img = mallocNew!(OwnedImage!RGBA);
634         scope(exit) destroyFree(img);
635         img.size(0, 0); // should be supported
636 
637         int border = 10;
638         img.size(0, 0, border); // should also be supported (border with no data!)
639         img.replicateBorders(); // should not crash
640 
641         border = 0;
642         img.size(1, 1, border);
643         img.replicateBorders(); // should not crash
644     }
645 }
646 
647 // Test border replication
648 unittest
649 {
650     OwnedImage!L8 img = mallocNew!(OwnedImage!L8);
651     scope(exit) destroyFree(img);
652     int width = 2;
653     int height = 2;
654     int border = 2;
655     int xMultiplicity = 1;
656     int trailing = 3;
657     img.size(width, height, border, xMultiplicity, trailing);
658     assert(img.w == 2 && img.h == 2);
659     assert(img.borderLeft() == 2 && img.borderRight() == 3);
660     assert(img._bytePitch == 7);
661 
662     img.fillWith(L8(5));
663     img.scanline(0)[0].l = 1;
664     img.scanlinePtr(0)[1].l = 2;
665     img[0, 1].l = 3;
666     img[1, 1].l = 4;
667     
668     img.replicateBorders();
669     ubyte[7][6] correct = 
670     [
671         [1, 1, 1, 2, 2, 2, 2],
672         [1, 1, 1, 2, 2, 2, 2],
673         [1, 1, 1, 2, 2, 2, 2],
674         [3, 3, 3, 4, 4, 4, 4],
675         [3, 3, 3, 4, 4, 4, 4],
676         [3, 3, 3, 4, 4, 4, 4],
677     ];
678 
679     for (int y = -2; y < 4; ++y)
680     {
681         for (int x = -2; x < 5; ++x)
682         {            
683             L8 read = img.unsafeScanlinePtr(y)[x];
684             ubyte good = correct[y+2][x+2];
685             assert(read.l == good);
686         }
687     }
688 }
689 
690 // Test toRef
691 unittest
692 {
693     OwnedImage!L8 img = mallocNew!(OwnedImage!L8);
694     scope(exit) destroyFree(img);
695 
696     int border = 0;
697     int rowAlignment = 1;
698     int xMultiplicity = 1;
699     int trailingSamples = 4;
700     img.size(10, 10, border, rowAlignment, xMultiplicity, trailingSamples); // Ask for 4 trailing samples
701     ImageRef!L8 r = img.toRef();
702     assert(r.pitch >= 10 + 4); // per the API, OwnedImage could add more space than required
703 }
704 
705 /// Loads an image from compressed data.
706 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`.
707 /// Throws: $(D ImageIOException) on error.
708 OwnedImage!RGBA loadOwnedImage(in void[] imageData)
709 {
710     Image image;
711     image.loadFromMemory(cast(const(ubyte[])) imageData, 
712                          LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS);
713     if (image.isError)
714     {
715         assert(false, "Decoding failed"); // FUTURE: could do something more sensible maybe
716     }
717 
718     return convertImageToOwnedImage_rgba8(image);
719 }
720 
721 
722 /// Loads two different images:
723 /// - the 1st is the RGB channels
724 /// - the 2nd is interpreted as greyscale and fetch in the alpha channel of the result.
725 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`.
726 /// Throws: $(D ImageIOException) on error.
727 OwnedImage!RGBA loadImageSeparateAlpha(in void[] imageDataRGB, in void[] imageDataAlpha)
728 {
729     Image alpha;
730     alpha.loadFromMemory(cast(const(ubyte[])) imageDataAlpha, 
731                          LOAD_GREYSCALE | LOAD_8BIT | LOAD_NO_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS);
732     if (alpha.isError)
733     {
734         assert(false, "Decoding failed"); // same remark as above
735     }
736 
737     Image rgb;
738     rgb.loadFromMemory(cast(const(ubyte[])) imageDataRGB, 
739                        LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS);
740     if (rgb.isError)
741     {
742         assert(false, "Decoding failed"); // same remark as above
743     }
744 
745     if ( (rgb.width != alpha.width) || (rgb.height != alpha.height) )
746     {
747         // If you fail here, typically size of your Diffuse map doesn't match the Emissive map.
748         assert(false, "Image size mismatch"); // same remark as above
749     }
750 
751     int W = rgb.width;
752     int H = rgb.height;
753 
754     for (int y = 0; y < H; ++y)
755     {
756         ubyte* scanA = cast(ubyte*) alpha.scanline(y);
757         ubyte* scanRGBA = cast(ubyte*) rgb.scanline(y);
758         for (int x = 0; x < W; ++x)
759         {
760             scanRGBA[4*x + 3] = scanA[x];
761         }
762     }
763 
764     return convertImageToOwnedImage_rgba8(rgb);
765 }
766 
767 
768 /// Loads an image to be a 16-bit one channel image (`OwnedImage!L16`).
769 /// The returned `OwnedImage!L16` should be destroyed with `destroyFree`.
770 /// Throws: $(D ImageIOException) on error.
771 OwnedImage!L16 loadOwnedImageDepth(in void[] imageData)
772 {
773     Image image;
774     image.loadFromMemory(cast(const(ubyte[])) imageData, LOAD_NO_ALPHA);
775     if (image.isError)
776     {
777         assert(false, "Decoding failed");
778     }
779   
780     if (image.type() == PixelType.rgb8)
781     {
782         // Legacy 8-bit to 16-bit depth conversion
783         // This is the original legacy way to load depth.
784         // Load as 8-bit RGBA, then mix the channels so as to support
785         // legacy depth maps.
786         Image image16;
787         image16.setSize(image.width, image.height, PixelType.l16, LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS);
788         int width = image.width;
789         int height = image.height;
790 
791         OwnedImage!L16 result = mallocNew!(OwnedImage!L16)(width, height);
792 
793         for (int j = 0; j < height; ++j)
794         {
795             ubyte* inDepth = cast(ubyte*) image.scanline(j);
796             ushort* outDepth = cast(ushort*) image16.scanline(j);
797 
798             for (int i = 0; i < width; ++i)
799             {
800                 // Using 257 to span the full range of depth: 257 * (255+255+255)/3 = 65535
801                 // If we do that inside stb_image, it will first reduce channels _then_ increase bitdepth.
802                 // Instead, this is used to gain 1.5 bit of accuracy for 8-bit depth maps. :)
803                 float d = 0.5f + 257 * (inDepth[3*i+0] + inDepth[3*i+1] + inDepth[3*i+2]) / 3; 
804                 outDepth[i] = cast(ushort)(d);
805             }
806         }
807         return convertImageToOwnedImage_l16(image16);
808     }
809     else
810     {
811         // Ensure the format pixel is l16, and the layout is compatible with OwnedImage
812         image.convertTo(PixelType.l16, LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS);
813         return convertImageToOwnedImage_l16(image);
814     }
815 }
816 
817 /// Make a gamut `Image` view from a dplug:graphics `ImageRef`.
818 /// The original `OwnedImage` is still the owner of the pixel data.
819 void createViewFromImageRef(COLOR)(ref Image view, ImageRef!COLOR source)
820 {
821     static if (is(COLOR == RGBA))
822     {
823         view.createViewFromData(source.pixels, source.w, source.h, PixelType.rgba8, cast(int)source.pitch);
824     }
825     else static if (is(COLOR == RGB))
826     {
827         view.createViewFromData(source.pixels, source.w, source.h, PixelType.rgb8, cast(int)source.pitch);
828     }
829     else static if (is(COLOR == L8))
830     {
831         view.createViewFromData(source.pixels, source.w, source.h, PixelType.l8, cast(int)source.pitch);
832     }
833     else static if (is(COLOR == L16))
834     {
835         view.createViewFromData(source.pixels, source.w, source.h, PixelType.l16, cast(int)source.pitch);
836     }
837     else
838         static assert(false);
839 }
840 
841 /// Convert and disown gamut Image to OwnedImage.
842 /// Result: ìmage` is disowned, result is owning the image data.
843 OwnedImage!RGBA convertImageToOwnedImage_rgba8(ref Image image)
844 {
845     assert(image.type() == PixelType.rgba8);
846     OwnedImage!RGBA r = mallocNew!(OwnedImage!RGBA);
847     r.w = image.width;
848     r.h = image.height;    
849     r._pixels = cast(RGBA*) image.scanline(0);
850     r._bytePitch = image.pitchInBytes();
851     r._buffer = image.disownData();
852     r._border = 0;      // TODO should keep it from input
853     r._borderRight = 0; // TODO should keep it from input
854     return r;
855 }
856 
857 ///ditto
858 OwnedImage!RGB convertImageToOwnedImage_rgb8(ref Image image)
859 {
860     assert(image.type() == PixelType.rgb8);
861     OwnedImage!RGB r = mallocNew!(OwnedImage!RGB);
862     r.w = image.width;
863     r.h = image.height;    
864     r._pixels = cast(RGB*) image.scanline(0);
865     r._bytePitch = image.pitchInBytes();
866     r._buffer = image.disownData();
867     r._border = 0;      // TODO should keep it from input
868     r._borderRight = 0; // TODO should keep it from input
869     return r;
870 }
871 
872 ///ditto
873 OwnedImage!L16 convertImageToOwnedImage_l16(ref Image image)
874 {
875     assert(image.type() == PixelType.l16);
876     OwnedImage!L16 r = mallocNew!(OwnedImage!L16);
877     r.w = image.width;
878     r.h = image.height;    
879     r._pixels = cast(L16*) image.scanline(0);
880     r._bytePitch = image.pitchInBytes();
881     r._buffer = image.disownData();
882     r._border = 0;      // TODO should keep it from input
883     r._borderRight = 0; // TODO should keep it from input
884     return r;
885 }
886 
887 ///ditto
888 OwnedImage!L8 convertImageToOwnedImage_l8(ref Image image)
889 {
890     assert(image.type() == PixelType.l8);
891     OwnedImage!L8 r = mallocNew!(OwnedImage!L8);
892     r.w = image.width;
893     r.h = image.height;    
894     r._pixels = cast(L8*) image.scanline(0);
895     r._bytePitch = image.pitchInBytes();
896     r._buffer = image.disownData();
897     r._border = 0;      // TODO should keep it from input
898     r._borderRight = 0; // TODO should keep it from input
899     return r;
900 }