Table of Contents

Class SvgCanvas

Namespace
Egami.Svg
Assembly
Egami.Svg.dll

A canvas that writes SVG: every drawing call becomes SVG elements, so anything drawn on a Canvas (shapes, strokes, gradients, images, text, clips, layers, recorded pictures) can be saved as a vector file that browsers, Inkscape and Egami.Svg all read. Gradients, patterns, clips, masks, images, glyph outlines and pictures are written once in defs and shared. What SVG cannot express (sweep gradients, noise and composed paints, meshes, layers with filters) is drawn by Egami into an image at FallbackScale and embedded as PNG.

The output is SVG 1.1 with CSS mix-blend-mode and isolation for blend modes and layers (and SVG 2's fr for a conical gradient whose start circle has a radius). Disposing the canvas finishes the file and writes it to the stream, which is left open. An image or picture drawn several times is stored once, as it was when first drawn.

public sealed class SvgCanvas : Canvas, IDisposable
Inheritance
SvgCanvas
Implements
Inherited Members
Extension Methods

Examples

// Anything drawn on a Canvas can be written as SVG: gradients, clips, images and pictures go once into defs.
using (SvgCanvas canvas = new(file, 200, 120, new SvgWriteOptions { Precision = 2, ImageFormat = SvgImageFormat.Png }))
{
    canvas.FillRoundRect(new RoundRect(new RectF(10, 10, 180, 100), 12), new LinearGradient(new Vector2(10, 0), new Vector2(190, 0), new RgbaF32(0.1f, 0.4f, 0.9f, 1), new RgbaF32(0.9f, 0.2f, 0.5f, 1)));
    canvas.SaveLayer(0.5f);
    canvas.StrokePath(Egami.Drawing.Path.Oval(new RectF(30, 30, 140, 60)), new Stroke(6) { Dash = new DashPattern([10, 5]) }, new Rgba8(255, 255, 255, 255));
    canvas.Restore();

    // A sweep gradient has no SVG form: it is drawn by Egami and embedded as a PNG (Svg.FallbackImage).
    canvas.FillCircle(new Vector2(100, 60), 20, new SweepGradient(new Vector2(100, 60), [new GradientStop(0, new RgbaF32(1, 1, 0, 1)), new GradientStop(1, new RgbaF32(0, 1, 1, 1))]));
}

// Disposing writes the file; the stream stays open.

Constructors

SvgCanvas(Stream, float, float, SvgWriteOptions?)

Initializes a new instance of the SvgCanvas class that writes one SVG file to a stream when disposed.

public SvgCanvas(Stream stream, float width, float height, SvgWriteOptions? options = null)

Parameters

stream Stream

Where the file goes, from its current position; left open.

width float

The drawing's width: the root's width and its viewBox's, in device units (CSS pixels).

height float

The drawing's height.

options SvgWriteOptions

How to write the file, or null for Default.

Examples

// Anything drawn on a Canvas can be written as SVG: gradients, clips, images and pictures go once into defs.
using (SvgCanvas canvas = new(file, 200, 120, new SvgWriteOptions { Precision = 2, ImageFormat = SvgImageFormat.Png }))
{
    canvas.FillRoundRect(new RoundRect(new RectF(10, 10, 180, 100), 12), new LinearGradient(new Vector2(10, 0), new Vector2(190, 0), new RgbaF32(0.1f, 0.4f, 0.9f, 1), new RgbaF32(0.9f, 0.2f, 0.5f, 1)));
    canvas.SaveLayer(0.5f);
    canvas.StrokePath(Egami.Drawing.Path.Oval(new RectF(30, 30, 140, 60)), new Stroke(6) { Dash = new DashPattern([10, 5]) }, new Rgba8(255, 255, 255, 255));
    canvas.Restore();

    // A sweep gradient has no SVG form: it is drawn by Egami and embedded as a PNG (Svg.FallbackImage).
    canvas.FillCircle(new Vector2(100, 60), 20, new SweepGradient(new Vector2(100, 60), [new GradientStop(0, new RgbaF32(1, 1, 0, 1)), new GradientStop(1, new RgbaF32(0, 1, 1, 1))]));
}

// Disposing writes the file; the stream stays open.

Exceptions

ArgumentNullException

stream is null.

ArgumentException

stream cannot be written.

ArgumentOutOfRangeException

A size is not positive and finite.

Properties

Options

Gets the options the file is written with.

public SvgWriteOptions Options { get; }

Property Value

SvgWriteOptions

Methods

Dispose(bool)

Releases the backend's resources; called once by Dispose() after every state is restored.

protected override void Dispose(bool disposing)

Parameters

disposing bool

Always true: the canvas has no finalizer.

OnClipPath(Path, ClipOp, bool)

Clips by a path's fill.

protected override void OnClipPath(Path path, ClipOp op, bool antialias)

Parameters

path Path

The path in local coordinates.

op ClipOp

Whether to keep the inside or the outside.

antialias bool

Whether 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

rect RectF

The rectangle in local coordinates, under Matrix.

op ClipOp

Whether to keep the inside or the outside.

antialias bool

Whether the edges are antialiased.

OnClipRoundRect(RoundRect, ClipOp, bool)

Clips by a round rectangle. By default it goes to OnClipPath(Path, ClipOp, bool).

protected override void OnClipRoundRect(RoundRect roundRect, ClipOp op, bool antialias)

Parameters

roundRect RoundRect

The round rectangle in local coordinates.

op ClipOp

Whether to keep the inside or the outside.

antialias bool

Whether the edges are antialiased.

OnDrawAnnotation(RectF, Annotation)

Marks an area with a link or a destination. By default nothing happens: pixels show no annotations.

protected override void OnDrawAnnotation(RectF area, Annotation annotation)

Parameters

area RectF

The area, in local coordinates under Matrix.

annotation Annotation

The link or destination.

OnDrawEncodedImage(EncodedImage, RectF, RectF, SamplingOptions, Paint?)

Draws part of an encoded image into a local rectangle; the source lies inside the image. A PDF backend can embed a JPEG's bytes as they are. By default the image is decoded once and drawn with OnDrawImage(Raster, RectF, RectF, SamplingOptions, Paint?).

protected override void OnDrawEncodedImage(EncodedImage image, RectF source, RectF destination, SamplingOptions sampling, Paint? paint)

Parameters

image EncodedImage

The image.

source RectF

The part to draw, inside the image.

destination RectF

Where it goes, in local coordinates.

sampling SamplingOptions

How it is sampled.

paint Paint

The blend mode, opacity and antialiasing, or null for source-over.

Examples

// A JPEG kept as its bytes: a PDF backend embeds them as they are; a raster canvas draws the decoded pixels.
EncodedImage jpeg = EncodedImage.Load("photo.jpg");
canvas.DrawImage(jpeg, new RectF(20, 20, 128, 96));

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

run GlyphRun

The glyphs, never empty.

origin Vector2

Where the run's origin goes, in local coordinates.

paint Paint

The paint.

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

image Raster

The image.

source RectF

The part to draw, inside the image.

destination RectF

Where it goes, in local coordinates.

sampling SamplingOptions

How it is sampled.

paint Paint

The 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}");
    }
}

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

mesh Mesh

The mesh, in local coordinates.

paint Paint

The paint.

colorBlend BlendMode

How the vertex colors combine with the paint.

valueStops ReadOnlySpan<GradientStop>

The stops vertex values map through, or empty.

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

path Path

The path in local coordinates.

stroke Stroke

The pen, or null to fill.

paint Paint

The 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}");
    }
}

OnDrawPicture(Picture, Matrix3x2, Paint?, bool)

Draws a recorded picture. A PDF backend can write it once as a form XObject and reference it each time. By default it saves the state, adds the matrix, starts a layer when the paint asks for one, replays the picture through the public calls (or draws its cached image) and restores.

protected override void OnDrawPicture(Picture picture, Matrix3x2 matrix, Paint? paint, bool cacheAsImage)

Parameters

picture Picture

The picture, with at least one call.

matrix Matrix3x2

The transform added before the canvas's.

paint Paint

The opacity and blend mode of the whole picture, or null.

cacheAsImage bool

Whether to draw the picture's cached image instead of its calls.

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()

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

options LayerOptions

How 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();
}