Class RasterCanvas<TPixel>
The raster backend: a canvas that draws into a raster's pixels, with exact-area antialiasing, blended through Egami's Compositor in the pixel type's own precision. A clip that is a pixel-aligned rectangle stays a rectangle; any other clip becomes a coverage mask over the clip's bounds. A call whose area exceeds 256 KiB of destination rows spreads its rows over up to MaxParallelism cores, with the same pixels at any core count.
A layer from SaveLayer(LayerOptions?) is premultiplied in the raster's class (PremulRgba8 for 8-bit rasters, PremulRgbaF32 for deeper ones) and covers only its bounds inside the clip, grown by its filter's reach; its memory comes from the raster's EgamiContext and is released at Restore().
public sealed class RasterCanvas<TPixel> : Canvas, IDisposable where TPixel : unmanaged, IPixelType<TPixel>
Type Parameters
TPixelThe raster's pixel type.
- Inheritance
-
RasterCanvas<TPixel>
- Implements
- Inherited Members
- Extension Methods
Examples
// A canvas draws onto a raster. Calls draw when they are made; dispose the canvas when done.
using Raster<Rgba8> raster = Raster.Create(400, 300, Rgba8.White);
using (RasterCanvas<Rgba8> canvas = raster.CreateCanvas())
{
canvas.FillRect(new RectF(20, 20, 160, 100), new Rgba8(30, 90, 200, 255));
// Save and Restore bracket transform and clip changes.
canvas.Save();
canvas.Translate(260, 150);
canvas.Rotate(30);
canvas.FillRect(new RectF(-50, -30, 100, 60), new Rgba8(200, 60, 30, 200));
canvas.Restore();
canvas.FillCircle(new Vector2(100, 220), 50, Rgba8.Black);
}
// Blend edges in linear light, which keeps thin lines and translucent colors from darkening.
using RasterCanvas<Rgba8> linear = raster.CreateCanvas(new CanvasOptions { ColorSpace = ColorSpace.Linear });
Constructors
RasterCanvas(Raster<TPixel>, CanvasOptions?)
Initializes a new instance of the RasterCanvas<TPixel> class over a raster's first frame.
public RasterCanvas(Raster<TPixel> raster, CanvasOptions? options = null)
Parameters
rasterRaster<TPixel>The raster to draw on.
optionsCanvasOptions
Examples
// A canvas draws onto a raster. Calls draw when they are made; dispose the canvas when done.
using Raster<Rgba8> raster = Raster.Create(400, 300, Rgba8.White);
using (RasterCanvas<Rgba8> canvas = raster.CreateCanvas())
{
canvas.FillRect(new RectF(20, 20, 160, 100), new Rgba8(30, 90, 200, 255));
// Save and Restore bracket transform and clip changes.
canvas.Save();
canvas.Translate(260, 150);
canvas.Rotate(30);
canvas.FillRect(new RectF(-50, -30, 100, 60), new Rgba8(200, 60, 30, 200));
canvas.Restore();
canvas.FillCircle(new Vector2(100, 220), 50, Rgba8.Black);
}
// Blend edges in linear light, which keeps thin lines and translucent colors from darkening.
using RasterCanvas<Rgba8> linear = raster.CreateCanvas(new CanvasOptions { ColorSpace = ColorSpace.Linear });
Properties
ColorGlyphRenderer
Gets the renderer for color glyph formats the canvas does not draw itself, such as OpenType SVG glyphs; none by default. A backend overrides it to draw those glyphs, as the raster backend returns ColorGlyphRenderer.
protected override IColorGlyphRenderer? ColorGlyphRenderer { get; }
Property Value
Options
Gets the canvas's settings.
public CanvasOptions Options { get; }
Property Value
Methods
Dispose(bool)
Releases the backend's resources; called once by Dispose() after every state is restored.
protected override void Dispose(bool disposing)
Parameters
OnClipPath(Path, ClipOp, bool)
Clips by a path's fill.
protected override void OnClipPath(Path path, ClipOp op, bool antialias)
Parameters
pathPathThe path in local coordinates.
opClipOpWhether to keep the inside or the outside.
antialiasboolWhether the edges are antialiased.
OnClipRect(RectF, ClipOp, bool)
Clips by a rectangle. By default it goes to OnClipPath(Path, ClipOp, bool).
protected override void OnClipRect(RectF rect, ClipOp op, bool antialias)
Parameters
rectRectFThe rectangle in local coordinates, under Matrix.
opClipOpWhether to keep the inside or the outside.
antialiasboolWhether the edges are antialiased.
OnDrawAtlas(Raster, ReadOnlySpan<SpriteTransform>, ReadOnlySpan<RectF>, ReadOnlySpan<RgbaF32>, BlendMode, SamplingOptions, Paint?)
Draws sprites from one image; the lists have equal lengths and colors is empty or as long. By default a sprite without a color is a DrawImage(Raster, RectF, RectF, SamplingOptions?, Paint?) under its transform, and a tinted one a two-triangle textured OnDrawMesh(Mesh, Paint, BlendMode, ReadOnlySpan<GradientStop>) call, one sprite at a time.
protected override void OnDrawAtlas(Raster image, ReadOnlySpan<SpriteTransform> transforms, ReadOnlySpan<RectF> sources, ReadOnlySpan<RgbaF32> colors, BlendMode colorBlend, SamplingOptions sampling, Paint? paint)
Parameters
imageRasterThe sprite sheet.
transformsReadOnlySpan<SpriteTransform>Where each sprite goes.
sourcesReadOnlySpan<RectF>Each sprite's part of the image.
colorsReadOnlySpan<RgbaF32>One color per sprite, or empty.
colorBlendBlendModeHow a sprite combines with its color.
samplingSamplingOptionsHow the image is sampled.
paintPaintThe blend mode, opacity and antialiasing, or null.
Examples
// Pack the sprites once, then draw thousands of them in one call: each with its own place, turn, scale and tint.
using SpriteSheet sheet = SpriteSheet.Pack([spark, star], new SpritePackOptions { Padding = 1 });
Random random = new(7);
SpriteTransform[] transforms = new SpriteTransform[2000];
RectF[] sources = new RectF[transforms.Length];
RgbaF32[] tints = new RgbaF32[transforms.Length];
for (int i = 0; i < transforms.Length; i++)
{
Rect frame = sheet.Frames[i % 2];
sources[i] = frame;
transforms[i] = SpriteTransform.FromRotationScale(
scale: 0.3f + random.NextSingle(),
degrees: random.NextSingle() * 360,
position: new Vector2(random.NextSingle() * 320, random.NextSingle() * 200),
anchor: new Vector2(frame.Width / 2f, frame.Height / 2f));
tints[i] = new RgbaF32(1, 0.5f + (random.NextSingle() / 2), random.NextSingle(), 0.8f);
}
using (RasterCanvas<Rgba8> canvas = raster.CreateCanvas())
{
// Plus adds the sprites up, as light does.
canvas.DrawAtlas(sheet.Image, transforms, sources, tints, BlendMode.Modulate, paint: new SolidPaint(Rgba8.White) { BlendMode = BlendMode.Plus });
}
Remarks
Each sprite is two textured triangles filled on its own; the sheet's texels are prepared once for the whole call.
OnDrawGlyphRun(GlyphRun, Vector2, Paint)
Draws a glyph run. The default turns the glyphs into one path, with the font's synthetic bold and italic, and calls OnDrawPath(Path, Stroke?, Paint), aliased for Alias, so every backend draws text; a backend with text of its own, such as a PDF page writing text operators, overrides it and can use Text and Clusters to keep the text searchable.
protected override void OnDrawGlyphRun(GlyphRun run, Vector2 origin, Paint paint)
Parameters
runGlyphRunThe glyphs, never empty.
originVector2Where the run's origin goes, in local coordinates.
paintPaintThe paint.
Remarks
Under a scale-and-translate transform, glyphs up to 256 device pixels per em are drawn from GlyphCache masks: with Subpixel their x origins round to quarter pixels (each phase its own mask) and y to whole pixels, else both to whole pixels; Hinting, Edging and synthetic bold and italic are applied when the mask is made, its curves flattened to 0.01 pixel (or the canvas tolerance when finer). Any other glyph run fills as one path at the canvas tolerance, with synthesis, aliased for Alias.
OnDrawImage(Raster, RectF, RectF, SamplingOptions, Paint?)
Draws part of an image into a local rectangle.
protected override void OnDrawImage(Raster image, RectF source, RectF destination, SamplingOptions sampling, Paint? paint)
Parameters
imageRasterThe image.
sourceRectFThe part to draw, inside the image.
destinationRectFWhere it goes, in local coordinates.
samplingSamplingOptionsHow it is sampled.
paintPaintThe blend mode, opacity and antialiasing, or null for source-over.
Examples
// A backend overrides hooks; the base class has already checked the arguments and tracks the transform and clip bounds.
// This one writes SVG elements, keeping rectangles as rectangles and strokes as stroke settings.
public sealed class SvgCanvas(float width, float height) : Canvas(width, height)
{
private readonly System.Text.StringBuilder _body = new();
public string Document => $"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{Width}\" height=\"{Height}\">{_body}</svg>";
protected override void OnDrawRect(RectF rect, Stroke? stroke, Paint paint)
=> _body.Append(FormattableString.Invariant($"<rect x=\"{rect.X}\" y=\"{rect.Y}\" width=\"{rect.Width}\" height=\"{rect.Height}\" {Style(stroke, paint)}/>"));
protected override void OnDrawPath(Path path, Stroke? stroke, Paint paint)
{
System.Text.StringBuilder d = new();
int p = 0;
foreach (PathVerb verb in path.Verbs)
{
int count = verb switch { PathVerb.Quad => 2, PathVerb.Cubic => 3, PathVerb.Close => 0, _ => 1 };
d.Append(verb switch { PathVerb.Move => "M", PathVerb.Line => "L", PathVerb.Quad => "Q", PathVerb.Cubic => "C", _ => "Z" });
foreach (Vector2 point in path.Points.Slice(p, count))
{
d.Append(FormattableString.Invariant($"{point.X},{point.Y} "));
}
p += count;
}
_body.Append(System.Globalization.CultureInfo.InvariantCulture, $"<path d=\"{d}\" {Style(stroke, paint)}/>");
}
protected override void OnClipPath(Path path, ClipOp op, bool antialias)
{
// Clips would become <clipPath> elements; this sketch leaves them out.
}
protected override void OnDrawImage(Raster image, RectF source, RectF destination, SamplingOptions sampling, Paint? paint)
{
// An image would be embedded as a PNG data URI.
}
private string Style(Stroke? stroke, Paint paint)
{
Rgba8 c = paint is SolidPaint solid ? solid.Color.ToRgba8() : Rgba8.Black;
string color = $"#{c.R:x2}{c.G:x2}{c.B:x2}";
string transform = Matrix.IsIdentity ? "" : FormattableString.Invariant($" transform=\"matrix({Matrix.M11} {Matrix.M12} {Matrix.M21} {Matrix.M22} {Matrix.M31} {Matrix.M32})\"");
return stroke is null
? $"fill=\"{color}\"{transform}"
: FormattableString.Invariant($"fill=\"none\" stroke=\"{color}\" stroke-width=\"{stroke.Width}\" stroke-linecap=\"{stroke.Cap.ToString().ToLowerInvariant()}\"{transform}");
}
}
OnDrawImageLattice(Raster, Lattice, RectF, SamplingOptions, Paint?)
Draws an image through a lattice; the lattice fits the image. By default each cell is an OnDrawImage(Raster, RectF, RectF, SamplingOptions, Paint?) call of its part of the image, sampled only inside that part, and each fixed-color cell an OnDrawRect(RectF, Stroke?, Paint) call; a PDF backend gets one image per cell this way. The raster backend draws the cells as one mesh instead, so they meet without seams.
protected override void OnDrawImageLattice(Raster image, Lattice lattice, RectF destination, SamplingOptions sampling, Paint? paint)
Parameters
imageRasterThe image.
latticeLatticeThe grid.
destinationRectFWhere the image goes, in local coordinates; not empty.
samplingSamplingOptionsHow the image is sampled.
paintPaintThe blend mode, opacity and antialiasing, or null.
Examples
using (RasterCanvas<Rgba8> canvas = raster.CreateCanvas())
{
// An Android .9.png: its one-pixel border marks what stretches. The corners stay sharp at any size.
(Raster inner, Lattice lattice, Rect? padding) = NinePatch.FromAndroid(file);
canvas.DrawImageLattice(inner, lattice, new RectF(10, 10, 180, 48));
inner.Dispose();
// A nine-patch from a plain image: the 8-pixel corners keep their size, the edges and center stretch.
canvas.DrawImageNinePatch(frame, new Rect(8, 8, 8, 8), new RectF(10, 70, 120, 40));
// A lattice can leave cells out or fill them with a color: here the center becomes a flat panel.
LatticeCellKind[] kinds = [.. Enumerable.Repeat(LatticeCellKind.Default, 9)];
kinds[4] = LatticeCellKind.FixedColor;
RgbaF32[] colors = new RgbaF32[9];
colors[4] = new RgbaF32(0.95f, 0.95f, 0.9f, 1);
canvas.DrawImageLattice(frame, new Lattice([8, 16], [8, 16]) { CellKinds = kinds, CellColors = colors }, new RectF(150, 70, 140, 40));
}
Remarks
The cells are one mesh, each cell's triangles textured from a copy of its part of the image, so samples stay inside the cell as in Skia and neighboring cells still meet without seams; fixed-color cells are part of the same mesh.
OnDrawImageMask(Raster, RectF, Paint, SamplingOptions)
Paints through an image mask. A PDF backend writes it as a stencil mask. By default the paint fills the rectangle in a layer and the mask's coverage, as an image drawn with DestinationIn, cuts it out.
protected override void OnDrawImageMask(Raster mask, RectF destination, Paint paint, SamplingOptions sampling)
Parameters
maskRasterThe mask image.
destinationRectFWhere the mask goes, in local coordinates; not empty.
paintPaintWhat fills the covered pixels.
samplingSamplingOptionsHow the mask is sampled.
Examples
// A 1-bit scan as a stencil: its white pixels paint in ink, its black ones leave the page as it is.
using Raster<Gray8> scan = Raster.Create(32, 16, new Gray8(255));
canvas.DrawImageMask(scan, new RectF(160, 20, 64, 32), new Rgba8(20, 40, 120, 255), new SamplingOptions { Filter = ResampleFilter.Nearest });
// A knockout group: each shape composites with what the group started as, so overlapping translucent shapes do not darken.
canvas.SaveLayer(new LayerOptions { Knockout = true });
canvas.FillCircle(new Vector2(180, 100), 20, new RgbaF32(1, 0, 0, 0.5f));
canvas.FillCircle(new Vector2(200, 100), 20, new RgbaF32(0, 0, 1, 0.5f));
canvas.Restore();
Remarks
The rectangle's coverage is multiplied by the mask's, sampled at each pixel, and the paint fills through it: no layer.
OnDrawMesh(Mesh, Paint, BlendMode, ReadOnlySpan<GradientStop>)
Draws a mesh with at least one triangle; a mesh with values comes with stops. A PDF backend can write it as a free-form or lattice-form shading (types 4 and 5). By default it is drawn through RasterizeFallback(Action<Canvas>, float) at one raster pixel per device unit, over the mesh's bounds, and placed with OnDrawImage(Raster, RectF, RectF, SamplingOptions, Paint?).
protected override void OnDrawMesh(Mesh mesh, Paint paint, BlendMode colorBlend, ReadOnlySpan<GradientStop> valueStops)
Parameters
meshMeshThe mesh, in local coordinates.
paintPaintThe paint.
colorBlendBlendModeHow the vertex colors combine with the paint.
valueStopsReadOnlySpan<GradientStop>The stops vertex values map through, or empty.
Remarks
The triangles' union is filled once through the exact-area rasterizer, so only its outer edges are antialiased and shared edges leave no cracks; each pixel takes its color from the triangle that owns it.
OnDrawPaint(Paint)
Fills the clip. By default it fills the device clip bounds mapped back to local coordinates.
protected override void OnDrawPaint(Paint paint)
Parameters
paintPaintThe paint.
Remarks
Fills the clip's whole pixels directly, through its mask when it has one.
OnDrawPath(Path, Stroke?, Paint)
Fills a path by its fill rule, or strokes it.
protected override void OnDrawPath(Path path, Stroke? stroke, Paint paint)
Parameters
pathPathThe path in local coordinates.
strokeStrokeThe pen, or null to fill.
paintPaintThe paint.
Examples
// A backend overrides hooks; the base class has already checked the arguments and tracks the transform and clip bounds.
// This one writes SVG elements, keeping rectangles as rectangles and strokes as stroke settings.
public sealed class SvgCanvas(float width, float height) : Canvas(width, height)
{
private readonly System.Text.StringBuilder _body = new();
public string Document => $"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{Width}\" height=\"{Height}\">{_body}</svg>";
protected override void OnDrawRect(RectF rect, Stroke? stroke, Paint paint)
=> _body.Append(FormattableString.Invariant($"<rect x=\"{rect.X}\" y=\"{rect.Y}\" width=\"{rect.Width}\" height=\"{rect.Height}\" {Style(stroke, paint)}/>"));
protected override void OnDrawPath(Path path, Stroke? stroke, Paint paint)
{
System.Text.StringBuilder d = new();
int p = 0;
foreach (PathVerb verb in path.Verbs)
{
int count = verb switch { PathVerb.Quad => 2, PathVerb.Cubic => 3, PathVerb.Close => 0, _ => 1 };
d.Append(verb switch { PathVerb.Move => "M", PathVerb.Line => "L", PathVerb.Quad => "Q", PathVerb.Cubic => "C", _ => "Z" });
foreach (Vector2 point in path.Points.Slice(p, count))
{
d.Append(FormattableString.Invariant($"{point.X},{point.Y} "));
}
p += count;
}
_body.Append(System.Globalization.CultureInfo.InvariantCulture, $"<path d=\"{d}\" {Style(stroke, paint)}/>");
}
protected override void OnClipPath(Path path, ClipOp op, bool antialias)
{
// Clips would become <clipPath> elements; this sketch leaves them out.
}
protected override void OnDrawImage(Raster image, RectF source, RectF destination, SamplingOptions sampling, Paint? paint)
{
// An image would be embedded as a PNG data URI.
}
private string Style(Stroke? stroke, Paint paint)
{
Rgba8 c = paint is SolidPaint solid ? solid.Color.ToRgba8() : Rgba8.Black;
string color = $"#{c.R:x2}{c.G:x2}{c.B:x2}";
string transform = Matrix.IsIdentity ? "" : FormattableString.Invariant($" transform=\"matrix({Matrix.M11} {Matrix.M12} {Matrix.M21} {Matrix.M22} {Matrix.M31} {Matrix.M32})\"");
return stroke is null
? $"fill=\"{color}\"{transform}"
: FormattableString.Invariant($"fill=\"none\" stroke=\"{color}\" stroke-width=\"{stroke.Width}\" stroke-linecap=\"{stroke.Cap.ToString().ToLowerInvariant()}\"{transform}");
}
}
OnDrawRect(RectF, Stroke?, Paint)
Fills or strokes a rectangle. By default it goes to OnDrawPath(Path, Stroke?, Paint).
protected override void OnDrawRect(RectF rect, Stroke? stroke, Paint paint)
Parameters
Remarks
An axis-aligned rectangle with a normal Blur is blurred exactly, from the error function.
OnRestore()
Called by Restore() after the state is popped; Matrix and DeviceClipBounds are already the restored ones.
protected override void OnRestore()
OnRestoreLayer()
Called by Restore() for a state saved by SaveLayer(LayerOptions?), after it is popped, so Matrix and DeviceClipBounds are those the layer was started with: the raster backend merges the layer through that clip. By default it calls OnRestore().
protected override void OnRestoreLayer()
Remarks
Filters the layer, multiplies it by its soft mask, and merges it with its opacity and blend mode through the restored clip.
OnSave()
Called by Save() after the state is pushed.
protected override void OnSave()
OnSaveLayer(LayerOptions)
Called by SaveLayer(LayerOptions?) after the state is pushed; DeviceClipBounds is already limited by the options' bounds. The raster backend allocates the layer; a PDF backend would begin a transparency group. By default it calls OnSave(), so a backend without layers draws their contents directly.
protected override void OnSaveLayer(LayerOptions options)
Parameters
optionsLayerOptionsHow the layer is merged.
Examples
using (RasterCanvas<Rgba8> canvas = raster.CreateCanvas())
{
// Group opacity: the overlapping circles fade as one shape, with no darker overlap.
canvas.SaveLayer(0.5f);
canvas.FillCircle(new Vector2(80, 100), 60, new Rgba8(200, 30, 30, 255));
canvas.FillCircle(new Vector2(140, 100), 60, new Rgba8(200, 30, 30, 255));
canvas.Restore();
// A group blended with Multiply and blurred as a whole; the layer grows by the blur's reach, so nothing is cut.
canvas.SaveLayer(new LayerOptions { BlendMode = BlendMode.Multiply, Filter = ops => ops.Blur(4) });
canvas.FillRect(new RectF(180, 40, 90, 120), new Rgba8(40, 120, 220, 255));
canvas.FillCircle(new Vector2(225, 100), 30, Rgba8.White);
canvas.Restore();
}
Remarks
Allocates the layer over the options' bounds inside the clip, grown by the filter's reach (the whole bounds, or the raster, when the reach is unknown), and starts it transparent, as a copy of the backdrop, or as the filtered backdrop. Inside the layer the clip is the layer's rectangle: the clip of this call applies once, when the layer is merged.