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);
OutputSizeis asked while the chain is planned, before any pixel changes. Throw there for arguments that cannot work.- When the size stays the same,
SourceandDestinationmay be the same plane (IsInPlace); read a row before you write it. ForEachBandruns bands of rows on up toEgamiContext.MaxParallelismthreads 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.Layouttells you the pixel type, so you can take a fast path for the types you know and fall back toToScaledandFromScaledfor the rest. CheckLayout.Alphabefore 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.