color library

Rikki Cattermole via Digitalmars-d digitalmars-d at puremagic.com
Sun Jun 21 08:42:37 PDT 2015


On Sunday, 21 June 2015 at 10:10:14 UTC, Rikki Cattermole wrote:
> On 21/06/2015 9:11 p.m., Manu via Digitalmars-d wrote:
>> I've been working on a submission for a std.color library.
>> I'm starting to become happy with it. I'd like to get API
>> criticism/review, such that if there are no more major 
>> changes, then
>> I'll start the fine-details and documenting.
>>
>> https://github.com/D-Programming-Language/phobos/pull/2845
>
> Following PR comments.
>
> Yeah I'm serious about you (Manu) watching me write a image 
> library using it tonight.
>
> I've had quite a bit of success live streaming and it would 
> work well for this and not just for publicizing D.
>
> Generally it makes you act out your emotions more. Making them 
> far more visible. Usability testing and emotions being visible 
> are quite useful for whoever is doing the testing. Just a 
> thought.

I did end up streaming, although Manu did not make it.

Had pretty good turn out though! Had quite a lot of questions 
about D code. Attributes and even comments (introduced DDOC).

Comments before code.
Here is what I learnt.

- Image storage is not the same as the file format storage
- Optimisations on the storage type are pretty easy
- The only thing that cannot be @nogc is constructors + file 
parsers
- Can we make GCAllocator (or what ever its name is) which Andrei 
is working on, you know, @nogc? I know I know, seems odd. But it 
would be _nice_ to make file parsers @nogc. That reminds me about 
other allocators? It just seems you know, a good idea here.
- The state of the color api is fine. As long as I can convert 
easily and access the values. Manu there is nothing so far which 
says ewww bad job! But really would be nice to see some actual 
code usage such as flipHorizontal and friends. Would be nice to 
see what happens with them. Especially there optimised versions. 
Okay there is one thing (next point), a function to determine if 
during conversion it 'leaks' information aka RGBA8 to RGB8 would. 
But not RGB8 to RGBA8.
- It's not hard to make an image storage type that wraps another 
so as long as you know what color type you need to be able to 
use, it can auto convert to whatever you want. Oh and look ma no 
allocation!
- There may be only a few image storage types implemented at 
_all_. Like I said previously image storage type != image file 
format. Struct + alias this works wonders here!

I'm genuinely quite excited by this. I think it could very well 
be a winner. It's very prone to no allocations and highly 
optimisable.
Manu you've done a great job so far. I can't fault you on it. I 
can only suggest one addition. Which is pretty amazing. 
Considering I haven't got to the point with this, where it is 
actually needed.
Also I can't stress this enough. Image storage type must and 
SHOULD be a different type to the file format struct. The file 
format struct should operate on the generic type. Which uses the 
color definition.

Anyway here is the code I wrote during the stream:
```D
import std.stdio;
import std.experimental.color;
import std.traits : ReturnType;
import std.typecons : tuple;

void main() {
	writeln("Edit source/app.d to start your project.");
}

/*
  * What does this solve?
  * - A general definition, get + set + width + height
  * - Memory allocation strategies
  * - File format support
  * - Range support
  * - Optimisation support per image storage for mutating 
functions e.g. flip
  * - Offset for image support
  * - General image storage wrapper
  *      - Auto conversion of colors
  *      - Adds offset support if not supported by original image 
storage type
  * - No allocating except in constructors!
  * - Similar to std.ranges in concept
  */

/*
  * Image definitions
  */

interface IImage(COLOR) {
     @property {
         size_t width() @nogc nothrow;
         size_t height() @nogc nothrow;
         void* storage() @nogc nothrow;
     }

     COLOR pixelAt(size_t x, size_t y) @nogc;
     void pixelStoreAt(size_t x, size_t y, COLOR value) @nogc;

     // COLOR opIndex(size_t x, size_t y)
     // void opIndexAssign(COLOR value, size_t x, size_t y)
}

// adds pixel offset capabilities to an image, useful for small 
images/embedded
interface IImagePixelOffset(COLOR) : IImage!COLOR {
     @property {
         size_t count() @nogc nothrow;
     }

     COLOR pixelAtOffset(size_t offset) @nogc;
     void pixelStoreAtOffset(size_t offset, COLOR value) @nogc;

     // COLOR opIndex(size_t offset)
     // void opIndexAssign(COLOR value, size_t offset)
}

/*
  * Traits for images
  */

bool isAnImage(T)() pure {
     static if (__traits(hasMember, T, "pixelAt")) {
         static if (!isColor!(ReturnType!(T.pixelAt)))
             return false;
     }

     static if (is(T == class) || is(T == struct)) {
         return __traits(hasMember, T, "width") && 
__traits(hasMember, T, "height") && __traits(hasMember, T, 
"pixelStoreAt") && __traits(hasMember, T, "storage");
     } else {
         return false;
     }
}

bool supportsPixelOffset(T)() pure {
     static if (!isAnImage!T) return false;

     static if (__traits(hasMember, T, "pixelAtOffset")) {
         static if (!is(ReturnType!(T.pixelAt) == 
ReturnType!(T.pixelAtOffset)))
             return false;
     }

     static if (is(T == class) || is(T == struct)) {
         return __traits(hasMember, T, "count") && 
__traits(hasMember, T, "pixelStoreAtOffset");
     } else {
         return false;
     }
}

/*
  * Some dummy images with different storage types + support for 
offset
  */

final class DummyImage(COLOR) {
     private {
         COLOR[][] data;
     }

     this(size_t width, size_t height, COLOR init = COLOR.init) {
         data.length = width;

         foreach(ref datem; data) {
             datem.length = height;
             datem[0 .. height] = init;
         }
     }

     @property {
         size_t width() @nogc nothrow {
             return data.length;
         }

         size_t height() @nogc nothrow
         in {
             assert(width > 0);
         } body {
             return data[0].length;
         }

         void* storage() @nogc nothrow { return 
cast(void*)data.ptr; }
     }

     COLOR pixelAt(size_t x, size_t y) @nogc
     in {
        assert(x < data.length);
        assert(y < data[0].length);
     } body {
         return data[x][y];
     }

     void pixelStoreAt(size_t x, size_t y, COLOR value) @nogc
     in {
        assert(x < data.length);
        assert(y < data[0].length);
     } body {
         data[x][y] = value;
     }
}

static assert(isAnImage!(DummyImage!RGB8));
static assert(!supportsPixelOffset!(DummyImage!RGB8));

final class DummyImage2(COLOR) {
     private {
         COLOR[][] data;
     }

     this(size_t width, size_t height, COLOR init)
     in {
         assert(width > 0);
         assert(height > 0);
     } body {
         data.length = width;

         foreach(ref datem; data) {
             datem.length = height;
             datem[0 .. height] = init;
         }
     }

     @property {
         size_t width() @nogc nothrow {
             return data.length;
         }

         size_t height() @nogc nothrow {
             return data[0].length;
         }

         size_t count() @nogc nothrow {
             return width * height;
         }

         void* storage() @nogc nothrow { return 
cast(void*)data.ptr; }
     }

     COLOR pixelAtOffset(size_t offset) @nogc {
         // width == 2
         // height == 3
         // offset == 3
         // x = 1
         // y = 0
         return pixelAt(offset % height, offset / (height + 1));
     }

     COLOR pixelAt(size_t x, size_t y) @nogc
     in {
        assert(x < data.length);
        assert(y < data[0].length);
     } body {
         return data[x][y];
     }

     void pixelStoreAt(size_t x, size_t y, COLOR value) @nogc
     in {
        assert(x < data.length);
        assert(y < data[0].length);
     } body {
         data[x][y] = value;
     }

     void pixelStoreAtOffset(size_t offset, COLOR value) @nogc {
         pixelStoreAt(offset % height, offset / (height + 1), 
value);
     }
}

static assert(isAnImage!(DummyImage2!RGB8));
static assert(supportsPixelOffset!(DummyImage2!RGB8));

final class DummyImage3(COLOR, size_t width_, size_t height_) {
     private {
         COLOR[height_][width_] data;
     }

     this(COLOR init) {
         foreach(ref datem; data) {
             datem[0 .. height_] = init;
         }
     }

     @property {
         size_t width() @nogc nothrow {
             return width_;
         }

         size_t height() @nogc nothrow {
             return height_;
         }

         void* storage() @nogc nothrow { return 
cast(void*)data.ptr; }
     }

     COLOR pixelAt(size_t x, size_t y) @nogc
     in {
        assert(x < width_);
        assert(y < height_);
     } body {
         return data[x][y];
     }
}

final class DummyImage4(COLOR, size_t width_, size_t height_) {
     private {
         COLOR[width_ * height_] data;
     }

     this(COLOR init) {
         datem[0 .. width_ * height_] = init;
     }

     @property {
         size_t width() @nogc nothrow {
             return width_;
         }

         size_t height() @nogc nothrow {
             return height_;
         }

         void* storage() @nogc nothrow { return 
cast(void*)data.ptr; }
     }

     COLOR pixelAt(size_t x, size_t y) @nogc
     in {
        assert(x < width_);
        assert(y < height_);
     } body {
         return data[(height_ * x) + y];
     }

     void pixelStoreAt(size_t x, size_t y, COLOR value) @nogc
     in {
        assert(x < width_);
        assert(y < height_);
     } body {
         data[(height_ * x) + y] = value;
     }
}

/*
  * Useful code
  */

// An image type that wraps others
// Auto converts to specified type
struct ImageOperable(COLOR) {
     @disable this();

     this(T)(T from) @nogc if (isAnImage!T)
     in {
         static if (is(T == class))
             assert(from !is null);
     } body {
         width = &from.width;
         height = &from.height;
         storage = &from.storage;

         static if (is(T == struct))
             origin_ = &from;
         else
             origin_ = cast(void*)from;

         auto tpixelAt = &from.pixelAt;
         pixelAt_ = &tpixelAt;
         pixelAt = &pixelAtCompatFunc!(ReturnType!(T.pixelAt));

         auto tpixelStoreAt = &pixelStoreAt;
         pixelStoreAt_ = &tpixelStoreAt;
         pixelStoreAt = 
&pixelStoreAtCompatFunc!(ReturnType!(T.pixelAt));

         static if (supportsPixelOffset!T) {
             count = &from.count;

             auto tpixelAtOffset = &from.pixelAtOffset;
             pixelAtOffset_ = &tpixelAtOffset;
             pixelAtOffset = 
&pixelAtOffsetCompatFunc!(ReturnType!(T.pixelAt));

             auto tpixelStoreAtOffset = &pixelStoreAtOffset;
             pixelStoreAtOffset_ = &tpixelStoreAt;
             pixelStoreAtOffset = 
&pixelStoreAtOffsetCompatFunc!(ReturnType!(T.pixelAt));
         } else {
             count = &countNotSupported;
             pixelAtOffset = &pixelAtOffsetNotSupported;
             pixelStoreAtOffset = &pixelStoreAtOffsetNotSupported;
         }
     }

     @property {
         size_t delegate() @nogc nothrow width;
         size_t delegate() @nogc nothrow height;
         size_t delegate() @nogc nothrow count;
         void* delegate() @nogc nothrow storage;
         void* original() @nogc nothrow { return origin_; }
     }

     COLOR delegate(size_t x, size_t y) @nogc pixelAt;
     void delegate(size_t x, size_t y, COLOR value) @nogc 
pixelStoreAt;

     COLOR delegate(size_t offset) @nogc pixelAtOffset;
     void delegate(size_t offset, COLOR value) @nogc 
pixelStoreAtOffset;

     private {
         void* origin_;

         void* pixelAt_;
         void* pixelStoreAt_;
         void* pixelAtOffset_;
         void* pixelStoreAtOffset_;

         COLOR pixelAtCompatFunc(FROM)(size_t x, size_t y) @nogc {
             auto del = *cast(FROM delegate(size_t x, size_t y) 
@nogc*) pixelAt_;

             static if (is(FROM == COLOR)) {
                 return del(x, y);
             } else {
                 return convertColor!COLOR(del(x, y));
             }
         }

         void pixelStoreAtCompatFunc(FROM)(size_t x, size_t y, 
COLOR value) @nogc {
             auto del = *cast(void delegate(size_t x, size_t y, 
FROM value) @nogc*) pixelStoreAt_;

             static if (is(FROM == COLOR)) {
                 del(x, y, value);
             } else {
                 del(x, y, convertColor!FROM(value));
             }
         }

         COLOR pixelAtOffsetCompatFunc(FROM)(size_t offset) @nogc {
             auto del = *cast(FROM delegate(size_t offset) @nogc*) 
pixelAtOffset_;

             static if (is(FROM == COLOR)) {
                 return del(offset);
             } else {
                 return convertColor!COLOR(del(offset));
             }
         }

         void pixelStoreAtOffsetCompatFunc(FROM)(size_t offset, 
COLOR value) @nogc {
             auto del = *cast(void delegate(size_t offset, FROM 
value) @nogc*) pixelStoreAtOffset_;

             static if (is(FROM == COLOR)) {
                 del(offset, value);
             } else {
                 del(offset, convertColor!FROM(value));
             }
         }

         size_t countNotSupported() @nogc nothrow {
             return width() * height();
         }

         COLOR pixelAtOffsetNotSupported(size_t offset) @nogc {
             return pixelAt(offset % height(), offset / (height() 
+ 1));
         }

         void pixelStoreAtOffsetNotSupported(size_t offset, COLOR 
value) @nogc {
             pixelStoreAt(offset % height(), offset / (height() + 
1), value);
         }
     }
}

// Constructs an input range over an image
// For every pixel get x + y and the color value
// Scan line version of this?
auto rangeOf(IMAGE)(IMAGE from) @nogc nothrow if 
(isAnImage!IMAGE) {
     struct RangeOf(IMAGE) {
         private {
             IMAGE input;
             size_t offsetX;
             size_t offsetY;
         }

         @property {
             auto front() @nogc {
                 return tuple!("x", "y", "value")(offsetX, 
offsetY, input.pixelAt(offsetX, offsetY));
             }

             bool empty() @nogc nothrow {
                 return offsetX == 0 && offsetY == input.height();
             }
         }

         auto save() @nogc nothrow {
             return RangeOf!IMAGE(input, offsetX, offsetY);
         }

         void popFront() @nogc nothrow {
             if (offsetX == input.width() - 1) {
                 offsetY++;
                 offsetX = 0;
             } else {
                 offsetX++;
             }
         }
     }

     return RangeOf!IMAGE(from);
}

/*
  * Some random thoughts on image formats
  */

struct ImageFormatPNG(COLOR) {
     ImageOperable!COLOR imageStorage;
     alias imageStorage this;

     string comment;
}

struct GCAllocator {}

// ImageStorage is the image storage e.g. DummyImage
// Image storage type != image format common misconception in 
implementations

ImageFormatPNG!COLOR readPNG(ImageStorage)(ubyte[] file) {
     return readPNG!(ImageStorage)(file, GCAllocator());
}

ImageFormatPNG!COLOR readPNG(ImageStorage, ALLOCATOR)(ubyte[] 
file, ALLOCATOR allocatorInstance) {
     alias COLOR = ReturnType!(ImageStorage.pixelAt);
     assert(0);
}

/*
  * Optimisation random thoughts
  */

void flipHorizontal(IMAGE)(IMAGE image, size_t maxRecursion = 
size_t.max) if (isAnImage!IMAGE) {
     doFrom(image);

     size_t recursionDone;

     void doFrom(IMAGE2)(IMAGE2 image2) {
         if (ImageOperable actualImage = cast(ImageOperable)image2 
&& recursionDone < maxRecursion) {
             alias COLOR = 
ReturnType!(typeof(actualImage).pixelAt);

             // unknown type irk
             doFrom(actualImage.actual);
             recursionDone++;
         } else if (DummyImage actualImage = 
cast(DummyImage)image2) {
             alias COLOR = 
ReturnType!(typeof(actualImage).pixelAt);
             // do something with actual.storage!
         } else {
             alias actualImage = image;
             alias COLOR = 
ReturnType!(typeof(actualImage).pixelAt);
             // do some generic algo using width, height, pixelAt 
and pixelStoreAt
         }
     }
}

/*
  * Unittests
  */

unittest {
     ImageOperable!RGB8 workaround = void;
     workaround = ImageOperable!(RGB8)(new DummyImage!RGB8(8, 3));
}

unittest {
     ImageOperable!RGBA8 workaround = void;
     workaround = ImageOperable!(RGBA8)(new DummyImage!RGB8(8, 3));
}

unittest {
     ImageOperable!RGBA8 workaround = void;
     workaround = ImageOperable!(RGBA8)(new DummyImage!RGB8(8, 3, 
RGB8(77, 82, 31)));
     size_t count = workaround.countNotSupported();
}

unittest {
     foreach(pixel; new DummyImage!RGB8(2, 2, RGB8(82, 32, 
11)).rangeOf) {
         assert(pixel.value == RGB8(82, 32, 11));
     }

     auto aRange = new DummyImage!RGB8(2, 2, RGB8(82, 32, 
11)).rangeOf;
     auto pixel = aRange.front;

     assert(pixel.x == 0);
     assert(pixel.y == 0);

     aRange.popFront;
     auto anotherRange = aRange.save();
     pixel = aRange.front;

     assert(aRange.front == anotherRange.front);
     aRange.popFront;
     assert(anotherRange.front == pixel);
}
```


More information about the Digitalmars-d mailing list