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