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);
}
```

Reply via email to