Migrating from SkiaSharp
If you are coming from SkiaSharp, the biggest adjustment is the rendering model. SkiaSharp code is usually centered on an SKCanvas supplied by the destination you are drawing to: a bitmap, raster surface, GPU surface, document, or picture recorder. ImageSharp.Drawing works inside the ImageSharp processing pipeline and records ordered drawing work through DrawingCanvas before replaying it to the active backend.
That difference is useful. The same drawing code can target normal CPU-backed images, retained backend scenes, and WebGPU-backed surfaces while keeping the same shape, brush, pen, text, and image composition model.
Start by matching behavior, not by chasing the shortest code. Keep the same canvas size, rectangles, colors, alpha, stroke widths, transform order, clipping, font files, and image sampling choices while translating the drawing model. Once the output matches, ImageSharp.Drawing usually lets you simplify because geometry, styling, text layout, and image processing are expressed as separate concepts.
Core Type Mapping
| SkiaSharp concept | ImageSharp.Drawing equivalent |
|---|---|
SKBitmap / SKImage / SKSurface |
Image<TPixel> for CPU images, or a WebGPU surface/render target for GPU output |
SKCanvas |
DrawingCanvas inside Paint(...), or a canvas created from an image frame or backend |
SKPaint fill |
Brush, usually Brushes.Solid(...), gradient brushes, image brushes, or pattern brushes |
SKPaint stroke |
Pen, usually Pens.Solid(...) or a custom Pen with stroke options |
SKColor |
Color, or a concrete pixel type such as Rgba32 when working directly with pixels |
SKRect / SKRoundRect |
Rectangle for rectangle fill, stroke, and clear helpers; RectangleF for APIs that explicitly accept floating-point bounds such as image destination rectangles; shape types when geometry must be reused |
SKPath |
PathBuilder, Path, IPath, and built-in shapes when geometry must be reused |
SKMatrix |
Matrix4x4 transforms, commonly constructed from Matrix3x2 |
SKImageFilter / SKMaskFilter |
Apply(...) with ImageSharp processors for region-scoped effects |
SKTextBlob / text drawing |
RichTextOptions, TextBlock, Fonts shaping, and DrawText(...) |
Drawing Targets and Paint Pipelines
In SkiaSharp, you draw through the SKCanvas provided by the current destination. A canvas backed by a raster bitmap or raster surface writes to pixels visible to the CPU. A GPU-surface canvas targets GPU work that is flushed or submitted later. A document or picture-recorder canvas records drawing commands instead of exposing writable pixels.
ImageSharp.Drawing gives you one ordered canvas API for these destination styles. Inside Paint(...), the canvas records drawing work at that point in the ImageSharp pipeline and replays it into the active backend when the processor completes.
For simple bitmap code, that often looks like this:
SkiaSharp:
using SkiaSharp;
using SKBitmap bitmap = new(420, 240);
using SKCanvas canvas = new(bitmap);
using SKPaint paint = new()
{
Color = SKColors.CornflowerBlue,
IsAntialias = true
};
canvas.Clear(SKColors.White);
canvas.DrawRect(SKRect.Create(40, 40, 260, 110), paint);
In ImageSharp.Drawing, draw inside an ImageSharp mutation pipeline:
ImageSharp.Drawing:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Image<Rgba32> image = new(420, 240, Color.White.ToPixel<Rgba32>());
image.Mutate(context => context.Paint(canvas =>
{
canvas.Fill(Brushes.Solid(Color.CornflowerBlue), new Rectangle(40, 40, 260, 110));
}));
Use Image.Mutate(...) when you want to modify an existing image. Use Image.Clone(...) when your old SkiaSharp code created a new output from an existing source while leaving the source unchanged.
Paint Becomes Brush and Pen
SkiaSharp uses SKPaint as a general drawing state object. The same type can represent fill, stroke, antialiasing, shaders, blend modes, filters, text settings, and more.
ImageSharp.Drawing splits those concepts into smaller objects:
Brushdescribes how an area is filled.Pendescribes how outlines are stroked.DrawingOptionscontrols antialiasing, transforms, blending, and shape behavior.RichTextOptionscontrols text layout and shaping.
SkiaSharp:
using SkiaSharp;
using SKPaint fill = new()
{
Color = SKColor.Parse("#2f80ed"),
IsAntialias = true,
Style = SKPaintStyle.Fill
};
using SKPaint stroke = new()
{
Color = SKColor.Parse("#1b3f72"),
IsAntialias = true,
StrokeWidth = 4,
Style = SKPaintStyle.Stroke
};
canvas.DrawRect(SKRect.Create(48, 42, 280, 126), fill);
canvas.DrawRect(SKRect.Create(48, 42, 280, 126), stroke);
ImageSharp.Drawing:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
image.Mutate(context => context.Paint(canvas =>
{
canvas.Fill(Brushes.Solid(Color.ParseHex("#2f80ed")), new Rectangle(48, 42, 280, 126));
canvas.Draw(Pens.Solid(Color.ParseHex("#1b3f72"), 4), new Rectangle(48, 42, 280, 126));
}));
This is usually the cleanest migration path: create brushes and pens where SkiaSharp code previously configured fill and stroke paints. Avoid looking for a single SKPaint replacement. In ImageSharp.Drawing, fill style belongs to the brush, stroke geometry belongs to the pen, graphics state belongs to DrawingOptions, and text layout belongs to RichTextOptions.
Paths and Shapes
SkiaSharp path code usually builds an SKPath, then fills or strokes it. ImageSharp.Drawing uses PathBuilder for incremental construction and IPath for the finished geometry.
Preserve whether each figure is open or closed. Closed figures define fillable areas and produce closed stroke joins; open figures are usually stroked outlines where cap behavior is visible at the ends. If the original Skia path relies on winding for holes, keep the same winding model or explicitly choose an IntersectionRule that matches the original fill type.
For direct migrations of simple SKCanvas calls, use the canvas helpers first. Rectangles use Fill(brush, Rectangle) and Draw(pen, Rectangle) overloads; ellipses, arcs, pies, lines, and Beziers have named helpers. Move to explicit shape objects when the same geometry is reused for fill, stroke, clipping, measurement, or composition.
SkiaSharp:
using SkiaSharp;
using SKPath triangle = new();
triangle.MoveTo(80, 180);
triangle.LineTo(160, 48);
triangle.LineTo(240, 180);
triangle.Close();
using SKPaint fill = new()
{
Color = SKColors.Gold,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
using SKPaint stroke = new()
{
Color = SKColors.DarkGoldenrod,
IsAntialias = true,
StrokeWidth = 4,
Style = SKPaintStyle.Stroke
};
canvas.DrawPath(triangle, fill);
canvas.DrawPath(triangle, stroke);
ImageSharp.Drawing:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
image.Mutate(context => context.Paint(canvas =>
{
PathBuilder builder = new();
builder.MoveTo(new PointF(80, 180));
builder.LineTo(new PointF(160, 48));
builder.LineTo(new PointF(240, 180));
builder.CloseFigure();
IPath triangle = builder.Build();
canvas.Fill(Brushes.Solid(Color.Gold), triangle);
canvas.Draw(Pens.Solid(Color.DarkGoldenrod, 4), triangle);
}));
For common one-off geometry, prefer the canvas helper that matches the original SKCanvas call:
SkiaSharp:
using SkiaSharp;
using SKPaint fill = new()
{
Color = SKColors.MediumSeaGreen,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
canvas.DrawOval(SKRect.Create(70, 72, 220, 96), fill);
ImageSharp.Drawing:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
image.Mutate(context => context.Paint(canvas =>
{
// ImageSharp.Drawing ellipse helpers take center and size, not top-left bounds.
canvas.FillEllipse(Brushes.Solid(Color.MediumSeaGreen), new(180, 120), new(220, 96));
}));
Use an explicit shape when the geometry is data, not just a drawing call. For example, an EllipsePolygon can be filled, stroked, clipped against, transformed, measured, or reused in several commands.
Transforms and Canvas State
SkiaSharp commonly uses Save(), Restore(), Translate(), Scale(), and RotateDegrees() on the canvas. ImageSharp.Drawing exposes the same scoped-state idea through Save(...), Restore(), and Matrix4x4 transforms.
Translate the transform in the same order that Skia applied it. The saved state affects subsequent geometry, strokes, text, clips, and image placement until it is restored. For ordinary 2D affine transforms, construct the ImageSharp.Drawing transform from Matrix3x2; the resulting Matrix4x4 keeps the public canvas model consistent across CPU, retained scene, and WebGPU targets.
SkiaSharp:
using SkiaSharp;
using SKPaint fillPaint = new()
{
Color = SKColors.HotPink,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
using SKPaint strokePaint = new()
{
Color = SKColors.White,
IsAntialias = true,
StrokeWidth = 3,
Style = SKPaintStyle.Stroke
};
canvas.Save();
canvas.Translate(210, 120);
canvas.Scale(1.2F, 0.8F);
canvas.DrawRect(SKRect.Create(-70, -24, 140, 48), fillPaint);
canvas.DrawRect(SKRect.Create(-70, -24, 140, 48), strokePaint);
canvas.Restore();
ImageSharp.Drawing:
using System.Numerics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
image.Mutate(context => context.Paint(canvas =>
{
DrawingOptions options = new()
{
Transform = new(
Matrix3x2.CreateScale(1.2F, 0.8F) *
Matrix3x2.CreateTranslation(210, 120))
};
_ = canvas.Save(options);
canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(-70, -24, 140, 48));
canvas.Draw(Pens.Solid(Color.White, 3), new Rectangle(-70, -24, 140, 48));
canvas.Restore();
}));
ImageSharp.Drawing uses Matrix4x4 because the same transform model works across CPU rendering, retained backend scenes, and WebGPU output. For normal 2D drawing, construct it from Matrix3x2 so the affine values stay familiar.
Image Composition
SkiaSharp image composition often uses DrawImage(...) or DrawBitmap(...). In ImageSharp.Drawing, use DrawImage(...) inside Paint(...) when the operation belongs with the rest of the drawing commands.
Keep the source and destination rectangles explicit while migrating. The source rectangle is in source-image coordinates; the destination rectangle is in canvas coordinates after the current transform. If the old Skia code used a specific sampling option, choose the matching ImageSharp resampler. Otherwise, the drawing API default is appropriate for normal resized placement.
SkiaSharp:
using SkiaSharp;
using SKBitmap source = SKBitmap.Decode("photo.jpg");
using SKBitmap output = new(640, 360);
using SKCanvas canvas = new(output);
using SKPaint strokePaint = new()
{
Color = SKColors.White,
IsAntialias = true,
StrokeWidth = 4,
Style = SKPaintStyle.Stroke
};
canvas.Clear(SKColors.White);
canvas.DrawBitmap(source, SKRect.Create(32, 32, 320, 220));
canvas.DrawRect(SKRect.Create(32, 32, 320, 220), strokePaint);
ImageSharp.Drawing:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Image<Rgba32> source = Image.Load<Rgba32>("photo.jpg");
using Image<Rgba32> output = new(640, 360, Color.White.ToPixel<Rgba32>());
output.Mutate(context => context.Paint(canvas =>
{
canvas.DrawImage(source, source.Bounds, new RectangleF(32, 32, 320, 220));
canvas.Draw(Pens.Solid(Color.White, 4), new Rectangle(32, 32, 320, 220));
}));
Keep source images alive until the canvas has replayed. Inside Paint(...), replay is owned by the processing operation. If you create and manage a canvas yourself, dispose it before disposing source images used by drawing commands.
Region Effects
SkiaSharp often applies blur, masking, or filters through paint filters or image filters. ImageSharp.Drawing uses Apply(...) to run normal ImageSharp processors inside a rectangle or path.
SkiaSharp:
using SkiaSharp;
using SKPaint shadowPaint = new()
{
Color = SKColors.Black.WithAlpha(89),
ImageFilter = SKImageFilter.CreateBlur(10, 10),
IsAntialias = true,
Style = SKPaintStyle.Fill
};
using SKPaint panelFillPaint = new()
{
Color = SKColors.White,
IsAntialias = true,
Style = SKPaintStyle.Fill
};
using SKPaint panelStrokePaint = new()
{
Color = SKColors.LightGray,
IsAntialias = true,
StrokeWidth = 1,
Style = SKPaintStyle.Stroke
};
canvas.DrawRect(SKRect.Create(70, 72, 280, 110), shadowPaint);
canvas.DrawRect(SKRect.Create(62, 58, 280, 110), panelFillPaint);
canvas.DrawRect(SKRect.Create(62, 58, 280, 110), panelStrokePaint);
ImageSharp.Drawing:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
image.Mutate(context => context.Paint(canvas =>
{
canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.35F)), new Rectangle(70, 72, 280, 110));
// Blur a larger region so the softened shadow can spread beyond the source rectangle.
canvas.Apply(new Rectangle(60, 62, 300, 130), region => region.GaussianBlur(10));
canvas.Fill(Brushes.Solid(Color.White), new Rectangle(62, 58, 280, 110));
canvas.Draw(Pens.Solid(Color.LightGray, 1), new Rectangle(62, 58, 280, 110));
}));
On GPU-backed canvases, Apply(...) may require readback into the CPU ImageSharp pipeline. Keep the affected region tight, just as you would keep Skia image filters scoped to the area that actually needs the effect.
Text
SkiaSharp text drawing can start simple, but richer layout usually involves SKTextBlob, font managers, shaping, and manual measurement. ImageSharp.Drawing uses SixLabors.Fonts directly, so advanced text layout is part of the normal drawing API.
The most common positioning difference is baseline versus layout origin. Skia's simple DrawText(...) overloads position text by baseline. ImageSharp.Drawing positions text through RichTextOptions, where Origin is interpreted by the chosen alignment and wrapping settings. When you need a top-left equivalent for Skia baseline code, account for font metrics during migration, then prefer RichTextOptions alignment once the output is confirmed.
SkiaSharp:
using SkiaSharp;
using SKTypeface typeface = SKTypeface.FromFile("Inter.ttf");
using SKFont font = new(typeface, 32);
using SKPaint paint = new()
{
Color = SKColors.Black,
IsAntialias = true
};
SKFontMetrics metrics;
font.GetFontMetrics(out metrics);
canvas.DrawText("Fast text layout for generated graphics", 48, 48 - metrics.Ascent, font, paint);
ImageSharp.Drawing:
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Processing;
image.Mutate(context => context.Paint(canvas =>
{
FontCollection collection = new();
FontFamily family = collection.Add("Inter.ttf");
Font font = family.CreateFont(32);
RichTextOptions options = new(font)
{
Origin = new PointF(48, 48)
};
canvas.DrawText(options, "Fast text layout for generated graphics", Brushes.Solid(Color.Black), pen: null);
}));
For manual line flow, measurement, caret movement, or rich spans, use the Fonts docs alongside the Drawing text guide.
Practical Migration Strategy
For most SkiaSharp migrations:
- Move bitmap load/save work to ImageSharp.
- Replace
SKCanvasdrawing blocks withimage.Mutate(context => context.Paint(canvas => ...)). - Replace fill
SKPaintobjects withBrushinstances. - Replace stroke
SKPaintobjects withPeninstances. - Replace
SKPathconstruction withPathBuilder,Path, or built-in shape types. - Replace canvas transform calls with saved canvas state and
Matrix4x4values constructed fromMatrix3x2. - Replace image filters with
Apply(...)where a normal ImageSharp processor gives the same effect. - Move text code to
RichTextOptions,TextBlock, and the Fonts layout APIs when measurement or wrapping matters.
You do not need to migrate everything at once. ImageSharp.Drawing is usually easiest to adopt by moving one rendering workflow at a time: generate the same output image, replace the paint/path/text concepts with the closest Drawing equivalents, then simplify once the new model is in place.
Practical Guidance
- Keep examples behavior-equivalent while migrating; change API shape first, then simplify.
- Move bitmap processing to core ImageSharp and canvas drawing to ImageSharp.Drawing.
- Replace mutable paint objects with explicit brushes, pens, and drawing options.
- Use saved canvas state for transforms, clipping, and scoped graphics options.
- Verify text output with real fonts and wrapping because SkiaSharp and Fonts use different layout models.