Table of Contents

Custom operations and formats

Everything here uses only Egami's public API; Egami's own test suite has an outside project that does each of these things.

Operations

An operation implements IRasterOperation: it says what size it makes and fills the destination rows.

/// <summary>Swaps red and blue, in parallel bands of rows.</summary>
public sealed class SwapRedBlue : IRasterOperation
{
    public Size OutputSize(Size input) => input;

    public void Run<TPixel>(OperationInput<TPixel> input)
        where TPixel : unmanaged, IPixelType<TPixel>
    {
        input.ForEachBand((start, end) =>
        {
            for (int y = start; y < end; y++)
            {
                Span<TPixel> source = input.Source.Row(y);
                Span<TPixel> destination = input.Destination.Row(y);
                for (int x = 0; x < source.Length; x++)
                {
                    Vector4 v = source[x].ToScaled();
                    destination[x] = TPixel.FromScaled(new Vector4(v.Z, v.Y, v.X, v.W));
                }
            }
        });
    }
}
image.Apply(ops => ops.Fit(100, 100).Add(new SwapRedBlue()), TestContext.Current.CancellationToken);
  • OutputSize is asked while the chain is planned, before any pixel changes. Throw there for arguments that cannot work.
  • When the size stays the same, Source and Destination may be the same plane (IsInPlace); read a row before you write it.
  • ForEachBand runs bands of rows on up to EgamiContext.MaxParallelism threads and checks the cancellation token between bands.
  • A step that changes the size updates the EXIF dimensions and drops the thumbnail, as the built-in resizes do.
  • TPixel.Layout tells you the pixel type, so you can take a fast path for the types you know and fall back to ToScaled and FromScaled for the rest. Check Layout.Alpha before you assume straight alpha.

To run a chain on part of a larger picture, such as a tile or a canvas layer, pad the part by how far the chain reads: RasterOps.Reach returns that as an OperationReach, or null when a step reads the whole image (a resize, a turn, a custom operation).

// How far a chain reads around each pixel: run it on a part of a picture padded by this much, and the part comes out as it
// would in the whole picture.
OperationReach? reach = RasterOps.Reach(ops => ops.Blur(3).Glow(4, Rgba8.White));
if (reach is OperationReach r)
{
    Console.WriteLine($"pad by {r.X} × {r.Y}");
}

// A resize reads the whole image: its reach is unknown.
Assert.Null(RasterOps.Reach(ops => ops.Resize(100, 100)));

Formats

A new file format plugs in through a FormatInfo with your own reader and writer; Your own format shows how.

Pixel types

A pixel type is an unmanaged struct implementing IPixelType<TSelf>. Its Layout says how it is stored: a layout Egami knows (Rgba with Unorm8, say) lets Egami take its fast paths; anything else uses PixelChannelOrder.Other, and Egami converts it only through its own ToScaled and FromScaled.

Wrapping memory

Raster.Wrap puts a raster over memory you own, without copying: a Memory<TPixel> with a stride in pixels, or a Memory<byte> with a stride in bytes, such as a video frame whose rows are padded. Pass an IDisposable owner to have the raster release the memory when it is disposed.

Rows and planes

For a one-off pass over pixels, ForEachRow runs a lambda on each row in parallel; no operation class needed.

using Raster<Rgba8> image = Raster.Create<Rgba8>(256, 64);

// A horizontal gradient; rows run in parallel, each row is a Span over the pixels.
image.ForEachRow((row, y) =>
{
    for (int x = 0; x < row.Length; x++)
    {
        row[x] = new Rgba8((byte)x, (byte)(y * 4), 128, 255);
    }
});

Each frame's pixels are a PixelPlane<TPixel>: rows of Span<TPixel>, and the whole frame as one span when it is stored in one block.

PixelPlane<Rgba8> plane = image.Frames[0].Plane;
if (plane.TryGetContiguous(out Span<Rgba8> all))
{
    all.Fill(Rgba8.White);  // one Span over every pixel when the rows are stored back to back
}
else
{
    for (int y = 0; y < plane.Height; y++)
    {
        plane.Row(y).Fill(Rgba8.White);
    }
}

RasterOps records each step of a chain; built-in steps and your own mix freely.

// RasterOps records steps; Apply runs them in one pass where it can, in place when the size stays.
using Raster photo = Raster.Load("egret.png");
photo.Apply(ops => ops
    .AutoOrient()
    .Cover(400, 300, Anchor.Center)
    .Saturation(1.1f)
    .Sharpen(1, 0.6f));
/// <summary>Black or white per pixel: ForEachBand gives each worker a band of rows from start to end.</summary>
private sealed class Threshold(float level) : IRasterOperation
{
    public Size OutputSize(Size input) => input;

    public void Run<TPixel>(OperationInput<TPixel> input)
        where TPixel : unmanaged, IPixelType<TPixel>
    {
        RowBandAction band = (start, end) =>
        {
            for (int y = start; y < end; y++)
            {
                Span<TPixel> source = input.Source.Row(y), destination = input.Destination.Row(y);
                for (int x = 0; x < source.Length; x++)
                {
                    Vector4 v = source[x].ToScaled();
                    float on = ((0.2126f * v.X) + (0.7152f * v.Y) + (0.0722f * v.Z)) >= level ? 1 : 0;
                    destination[x] = TPixel.FromScaled(new Vector4(on, on, on, v.W));
                }
            }
        };
        input.ForEachBand(band);
    }
}

Recipes

Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.

Interop with raw buffers

// Pixels from somewhere else, such as a camera SDK: 640 × 480 BGRA, rows 2,560 bytes apart.
byte[] buffer = new byte[2560 * 480];

// Wrap them without copying, work on them, and save.
using (Raster<Bgra8> frame = Raster.Wrap<Bgra8>(buffer, 640, 480, byteStride: 2560))
{
    frame.Apply(ops => ops.FillColor(Rgba8.Black).Opacity(1));
    frame.Save("frame.png");
}

// Or copy the pixels out of a raster into your own buffer, row by row.
using Raster<Rgba8> image = Raster.Load<Rgba8>("duck.png");
Rgba8[] pixels = new Rgba8[image.Width * image.Height];
for (int y = 0; y < image.Height; y++)
{
    image.Row(y).CopyTo(pixels.AsSpan(y * image.Width, image.Width));
}

Run it

samples/Extending is a console app that does what this guide shows on the bundled images: dotnet run --project samples/Extending writes its results to ./output.