Compositing
DrawImage draws one raster onto another with source-over blending: the normal way a layer covers what is under it.
// Source-over blending, clipped to the overlap; the logo may be any pixel type.
photo.Apply(ops => ops.DrawImage(logo, new Point(photo.Width - logo.Width - 10, 10), opacity: 0.9f), Ct);
- The image may sit partly or wholly outside; only the overlap changes.
- It may be any pixel type; its first frame is drawn onto every frame of the target.
opacitymultiplies its alpha: 1 draws it as it is, 0 draws nothing.- For
Rgba8andBgra8targets the blend is exact integer math: every result is the true value rounded to nearest.
Part of an image
// Draw part of an image, such as one tile of a sprite sheet: no copy is made.
Rect tile = new(0, 0, 32, 32);
photo.Apply(ops => ops.DrawImage(logo, tile, new Point(10, 10)), Ct);
The area is clipped to the image; an area that does not overlap it throws ArgumentOutOfRangeException. Drawing an area costs
the same as drawing an image of that size.
Blend modes
Pass a BlendMode to choose how each pixel combines with the one under it: the twelve Porter–Duff operators, Plus and Modulate,
and the W3C blend modes from Multiply to Luminosity, plus Add and Subtract and the rest of Photoshop's modes: Divide,
LinearBurn, LinearLight, VividLight, PinLight, HardMix, DarkerColor, LighterColor and Dissolve. Dissolve draws each pixel fully or
not at all, with a chance equal to its alpha, from a fixed hashed pattern of the pixel's place (not Photoshop's pattern).
// Print the photo onto paper: Multiply keeps the paper's texture under every color.
paper.Apply(ops => ops.DrawImage(photo, new Point(0, 0), BlendMode.Multiply), Ct);
// Or lighten it in linear light at half strength; only where the photo overlaps changes.
paper.Apply(ops => ops.DrawImage(photo, new Point(10, 10), BlendMode.Screen, 0.5f, ColorSpace.Linear), Ct);
- Only the overlap changes, as when drawing a layer:
DestinationIncuts the image's shape out of the overlap and leaves the rest. SourceOveron stored values is exactly the plainDrawImage.- Blends use the stored values, as browsers and editors do; pass
ColorSpace.Linearto blend light instead, with the raster's own curve. - On
PremulRgba8,PremulBgra8,Rgba8andBgra8the Porter–Duff operators, Plus, Modulate and the product-and-sum modes are exact integer math; ColorDodge, ColorBurn, SoftLight, the Photoshop modes and the non-separable modes are within a level of exact, with alpha exact.
// One enum names every mode. Blend modes (Multiply to Luminosity) blend the colors, then cover what is under them.
BlendMode[] darkening = [BlendMode.Multiply, BlendMode.Darken, BlendMode.ColorBurn];
BlendMode[] cutting = [BlendMode.DestinationIn, BlendMode.DestinationOut]; // shape the destination by the image's alpha
Blend a color
// Warm the photo with an orange tint, and give it a gray-blue color grade.
photo.Apply(ops => ops
.BlendColor(new Rgba8(255, 200, 150, 255), BlendMode.Multiply)
.BlendColor(new Rgba8(60, 80, 110, 255), BlendMode.Color, opacity: 0.3f), Ct);
BlendColor is DrawImage of a solid image the size of the raster, without making one.
Arithmetic composite
// Average two images: each channel becomes 0.5·S + 0.5·D.
photo.Apply(ops => ops.DrawArithmetic(other, new Point(0, 0), k1: 0, k2: 0.5f, k3: 0.5f, k4: 0), Ct);
Each premultiplied channel becomes k1·S·D + k2·S + k3·D + k4, as SVG's feComposite arithmetic operator and Skia's arithmetic
blender define it, kept a valid premultiplied pixel unless clamp is false.
Blend rows in your own code
Compositor runs the same kernels on spans of any pixel type, with optional per-pixel coverage for antialiased edges. It allocates
nothing once warm and is safe to call from many threads.
// Blend rows in your own code: a source row, a destination row, and coverage from an antialiased edge.
Rgba8[] source = new Rgba8[256], destination = new Rgba8[256];
byte[] coverage = new byte[256];
for (int x = 0; x < 256; x++)
{
source[x] = new Rgba8((byte)x, 80, 200, 255);
destination[x] = new Rgba8(250, 250, 240, 255);
coverage[x] = (byte)Math.Clamp((x - 100) * 8, 0, 255); // a soft edge from x = 100 to 132
}
Compositor.Blend<Rgba8>(BlendMode.Multiply, source, destination, coverage);
// One color through coverage, the way a rasterizer fills a shape's spans.
Compositor.Blend(BlendMode.SourceOver, new Rgba8(255, 0, 0, 128), destination.AsSpan(), coverage);
Recipes
Short answers to common tasks in this area. Each one compiles and runs in Egami's test run.
Watermark with Multiply
// A dark logo that should read as printed on the photo: Multiply darkens by the logo's colors and never adds white.
using Raster<Rgba8> logo = Raster.Create(120, 40, new Rgba8(40, 60, 120, 255));
photo.Apply(ops => ops.DrawImage(logo, new Point(photo.Width - 140, photo.Height - 60), BlendMode.Multiply, opacity: 0.6f), Ct);
A dark logo drawn with Multiply reads as ink on the photo: light areas of the logo leave the photo unchanged.
Blend a row in a custom operation
/// <summary>A custom step that screens a horizontal gradient over each row, blending with the Compositor.</summary>
public sealed class ScreenGradient : IRasterOperation
{
public Size OutputSize(Size input) => input;
public void Run<TPixel>(OperationInput<TPixel> input)
where TPixel : unmanaged, IPixelType<TPixel>
{
int width = input.Destination.Width;
TPixel[] gradient = new TPixel[width];
for (int x = 0; x < width; x++)
{
gradient[x] = TPixel.FromRgba8(new Rgba8(255, 220, 160, (byte)(255 * x / Math.Max(1, width - 1))));
}
input.ForEachBand((start, end) =>
{
for (int y = start; y < end; y++)
{
input.Source.Row(y).CopyTo(input.Destination.Row(y));
Compositor.Blend<TPixel>(BlendMode.Screen, gradient, input.Destination.Row(y));
}
});
}
}
Compositor.Blend works on any pixel type, so one custom step serves every raster.
Run it
samples/Compositing is a console app that does what this guide shows on the bundled
images: dotnet run --project samples/Compositing writes its results to ./output.