---
title: "Migrate from SkiaSharp to OneImage"
url: https://recursiveai.org/oneimage/articles/migrate-from-skiasharp/
description: "Port SkiaSharp code to OneImage: SKBitmap and SKCanvas to Image and Canvas, resize, crop, EXIF orientation, encoders, drawing, text, SVG and ASP.NET Core."
date: 2026-10-26
author: OneImage team
---

# Migrate from SkiaSharp to OneImage

Port SkiaSharp code to OneImage: SKBitmap and SKCanvas to Image and Canvas, resize, crop, EXIF orientation, encoders, drawing, text, SVG and ASP.NET Core.

October 26, 2026 · OneImage team

To migrate from SkiaSharp to OneImage, you replace a native graphics engine with one managed .NET library. `SKBitmap`, `SKImage` and `SKCodec` become a single immutable `Image`. `SKSurface` and `SKCanvas` become a `Canvas`. `SKPaint` splits into a `Brush` for fills and a `Pen` for strokes, and there’s no `libSkiaSharp` binary to ship for each platform. This page puts common SkiaSharp code next to its OneImage equivalent. Every sample on both sides was compiled and run on .NET 10 on Linux x64, using SkiaSharp 4.153.1 (with `SkiaSharp.NativeAssets.Linux.NoDependencies` 4.153.1 and Svg.Skia 5.2.3) and OneImage 1.10.1 and its add-on packages, all on the same input files.

Most teams looking for a SkiaSharp alternative want to stop deploying native assets. The [SkiaSharp 4.153.1](https://www.nuget.org/packages/SkiaSharp/4.153.1) package lists only the macOS and Windows native-asset packages as dependencies for `net10.0`. Its [package documentation](https://github.com/mono/SkiaSharp/blob/main/documentation/dev/packages.md) says the Linux assets must be added manually, to the application project. Our [SkiaSharp on Linux and Docker](https://recursiveai.org/oneimage/articles/skiasharp-linux-docker/) guide covers the errors that follow when they aren’t. If you use SkiaSharp for GPU rendering or UI controls, read [what SkiaSharp has that OneImage doesn’t](#what-skiasharp-has-that-oneimage-doesnt) first.

## Swap the packages

*Terminal · NuGet*

```
dotnet remove package SkiaSharp
dotnet remove package SkiaSharp.NativeAssets.Linux.NoDependencies   # or SkiaSharp.NativeAssets.Linux
dotnet remove package Svg.Skia                                      # if you render SVG
 
dotnet add package RecursiveAI.OneImage          # decode, resize, encode, drawing, Canvas
dotnet add package RecursiveAI.OneImage.Text     # fonts and text
dotnet add package RecursiveAI.OneImage.Svg      # SVG input
dotnet add package RecursiveAI.OneImage.Pdf      # PDF pages as images
dotnet add package RecursiveAI.OneImage.Web      # ASP.NET Core middleware
```

Add only the packages you need. Replace `using SkiaSharp;` with `using OneImage;`, plus `using OneImage.Drawing;` for paths, brushes and pens. OneImage targets `net10.0` only and is licensed MIT OR Apache-2.0. SkiaSharp’s NuGet listing also covers .NET Framework 4.6.2, .NET Standard 2.0 and the mobile targets, so check your target frameworks before you start. The [getting-started page](https://recursiveai.org/oneimage/docs/getting-started/) has the install details.

## Concepts that differ

| SkiaSharp | OneImage |
|---|---|
| `SKBitmap`: a decoded, mutable raster in memory | `Image`: an immutable description of a pipeline; pixels are computed tile by tile when you save or read them |
| `SKImage`: immutable, possibly backed by encoded data or the GPU | The same `Image`; there is one image type |
| `SKCodec` for the header, the EXIF origin and scaled decoding | `Image.Identify` for the header; orientation is applied at `Load`; a JPEG is decoded at 1/2, 1/4 or 1/8 scale automatically when the output is small enough |
| `SKSurface` + `SKCanvas` to draw | `Canvas`, a mutable tiled surface, or the same drawing calls on an `Image`, recorded lazily |
| `SKPaint` carries color, style, stroke width and effects | A `Brush` (fills) or `Pen` (strokes) passed to each call |
| `Encode(format, quality)` returns `SKData` | `Save(path or stream)`, with options in `SaveOptions` |
| `SKColor`, color types such as `Bgra8888` and `Rgba8888`, premultiplied or not | `Rgba32` and other pixel structs, which appear only when you read or write pixels |
| Native `libSkiaSharp` per OS and architecture | One managed assembly, the same on every platform |

Microsoft’s API reference describes [`SKBitmap`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skbitmap) as a raster bitmap. It says the content of an [`SKImage`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skimage) “is always immutable” and that it may store its data on the GPU or as drawing commands. OneImage has one `Image` type. Each operation (`Resize`, `Crop`, `Fill`) returns a new `Image` that records what to do, and nothing is decoded until you save it or read its pixels. That’s how a thumbnail of a large JPEG avoids holding the full-size picture in memory. The [OneImage guide](https://recursiveai.org/oneimage/docs/guide/) explains the lazy model.

### Disposal

In SkiaSharp 4.153.1, `SKBitmap`, `SKImage`, `SKCanvas`, `SKSurface`, `SKPaint`, `SKPath`, `SKData`, `SKCodec`, `SKFont` and `SKTypeface` all derive from [`SKNativeObject`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.sknativeobject) and are `IDisposable`, because each wraps a native object. That’s why SkiaSharp code is full of `using`. In OneImage, `Image` has no `Dispose`. Only `Canvas` is disposable, and disposing it returns its pooled tile memory. Forgetting to dispose a canvas is safe, because the garbage collector reclaims the memory, just later.

### Native assets

We published the resize program below for `linux-x64` with each library. The SkiaSharp build carried a 12.2 MB `libSkiaSharp.so` from the NoDependencies package next to `SkiaSharp.dll`. Published without a runtime identifier, its `runtimes` folder held a `libSkiaSharp` build for each of 17 OS and architecture combinations. The OneImage build carried `OneImage.dll` (1.5 MB), the same file on Windows, macOS, Linux, Alpine and ARM. There’s no NativeAssets package to pick, no fontconfig question, and nothing to install in a container image. OneImage’s decoders are bounds-checked C#, and it supports trimming and NativeAOT.

## Load, resize and save

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKBitmap input = SKBitmap.Decode("photo.jpg");
int height = input.Height * 800 / input.Width;
 
using SKBitmap resized = input.Resize(new SKImageInfo(800, height), new SKSamplingOptions(SKCubicResampler.Mitchell));
using SKData jpeg = resized.Encode(SKEncodedImageFormat.Jpeg, 85);
File.WriteAllBytes("photo-800.jpg", jpeg.ToArray());
 
Console.WriteLine($"{resized.Width}x{resized.Height}");
```

*OneImage · 1.10.1 · .NET 10*

```
using OneImage;
 
Image resized = Image.Load("photo.jpg").Resize(width: 800);
resized.Save("photo-800.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 85 } });
 
Console.WriteLine($"{resized.Width}x{resized.Height}");
```

From our 3000×3002 photo, SkiaSharp wrote 800×800 and OneImage wrote 800×801. The integer division truncates 800.53 to 800, and OneImage rounds the computed side to the nearest pixel. Pass `height:` as well if you need an exact size. With only `width:`, the aspect ratio is kept. The output format follows the file extension.

`SKSamplingOptions` maps to the `filter:` argument of `Resize`:

| SkiaSharp | OneImage `filter:` |
|---|---|
| `new SKSamplingOptions(SKFilterMode.Nearest)` | `ResamplingFilter.Nearest` |
| `new SKSamplingOptions(SKFilterMode.Linear)` | `ResamplingFilter.Bilinear` |
| `new SKSamplingOptions(SKCubicResampler.Mitchell)` | `ResamplingFilter.Mitchell` |
| `new SKSamplingOptions(SKCubicResampler.CatmullRom)` | `ResamplingFilter.CatmullRom` |
| (no equivalent) | `ResamplingFilter.Auto`, the default: Lanczos3 when shrinking, Catmull-Rom when enlarging |

SkiaSharp has no notion of fitting a box, so code that crops to fill a square usually computes a source rectangle and calls `DrawBitmap`. In OneImage that’s a single call, `Resize(width: 400, height: 400, fit: Fit.Cover)`. `Fit.Contain` (the default), `Fit.Pad` with `background:`, and `Fit.Stretch` cover the other cases, and `anchor:` chooses which part a crop keeps. OneImage resamples in linear light, so its output can differ slightly from SkiaSharp’s at edges and in gradients. Our guide to [resizing images in C#](https://recursiveai.org/oneimage/articles/resize-image-csharp/) has more.

## Crop

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKImage image = SKImage.FromEncodedData("photo.jpg");
using SKImage cropped = image.Subset(SKRectI.Create(100, 80, 600, 400));
using SKData png = cropped.Encode(SKEncodedImageFormat.Png, 100);
File.WriteAllBytes("crop.png", png.ToArray());
 
Console.WriteLine($"{cropped.Width}x{cropped.Height}");
```

*OneImage · 1.10.1 · .NET 10*

```
using OneImage;
 
Image cropped = Image.Load("photo.jpg").Crop(100, 80, 600, 400);
cropped.Save("crop.png");
 
Console.WriteLine($"{cropped.Width}x{cropped.Height}");
```

Both wrote a 600×400 PNG, and the two crops were pixel-identical. `SKRectI.Create(x, y, width, height)` becomes four integers or an `ImageRect`, with the same meaning. OneImage never decodes the tiles outside a crop. See [cropping an image in C#](https://recursiveai.org/oneimage/articles/crop-image-csharp/) for crops by aspect ratio and around a focal point.

## EXIF orientation and rotation

`SKBitmap.Decode` returns the pixels in their stored layout. A phone photo taken in portrait often comes out sideways, because the camera stored it landscape and recorded the turn in the EXIF orientation tag. SkiaSharp reports that tag as [`SKCodec.EncodedOrigin`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skcodec.encodedorigin), and applying it is up to you:

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKCodec codec = SKCodec.Create("portrait.jpg");
using SKBitmap stored = SKBitmap.Decode(codec);
SKEncodedOrigin origin = codec.EncodedOrigin;
 
// Origins 5 to 8 (LeftTop .. LeftBottom) swap width and height.
bool swap = (int)origin >= 5;
int w = swap ? stored.Height : stored.Width;
int h = swap ? stored.Width : stored.Height;
 
SKMatrix matrix = origin switch
{
    SKEncodedOrigin.TopRight    => new SKMatrix(-1, 0, w, 0, 1, 0, 0, 0, 1),
    SKEncodedOrigin.BottomRight => new SKMatrix(-1, 0, w, 0, -1, h, 0, 0, 1),
    SKEncodedOrigin.BottomLeft  => new SKMatrix(1, 0, 0, 0, -1, h, 0, 0, 1),
    SKEncodedOrigin.LeftTop     => new SKMatrix(0, 1, 0, 1, 0, 0, 0, 0, 1),
    SKEncodedOrigin.RightTop    => new SKMatrix(0, -1, w, 1, 0, 0, 0, 0, 1),
    SKEncodedOrigin.RightBottom => new SKMatrix(0, -1, w, -1, 0, h, 0, 0, 1),
    SKEncodedOrigin.LeftBottom  => new SKMatrix(0, 1, 0, -1, 0, h, 0, 0, 1),
    _                           => SKMatrix.Identity,
};
 
using var upright = new SKBitmap(w, h);
using (var canvas = new SKCanvas(upright))
{
    canvas.SetMatrix(matrix);
    canvas.DrawBitmap(stored, 0, 0, SKSamplingOptions.Default);
}
 
using SKData jpeg = upright.Encode(SKEncodedImageFormat.Jpeg, 90);
File.WriteAllBytes("portrait-upright.jpg", jpeg.ToArray());
 
Console.WriteLine($"{origin}: stored {stored.Width}x{stored.Height}, upright {upright.Width}x{upright.Height}");
```

*OneImage · 1.10.1 · .NET 10*

```
using OneImage;
 
// Load applies the EXIF orientation: Width and Height are the upright size.
Image upright = Image.Load("portrait.jpg");
upright.Save("portrait-upright.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 90 } });
 
// An explicit turn or mirror is one call.
upright.Rotate(90).Flip(FlipAxis.Horizontal).Save("portrait-turned.png");
 
Console.WriteLine($"upright {upright.Width}x{upright.Height}");
```

Our test photo is stored as 1200×800 with orientation 6. SkiaSharp printed `RightTop: stored 1200x800, upright 800x1200`, and OneImage printed `upright 800x1200`. The two upright images were pixel-identical. We also ran the SkiaSharp matrix table on all eight orientations, and each result matched the upright reference. When you port, delete the `EncodedOrigin` code. If it stays in front of OneImage, the picture is turned twice. To get the stored layout and the original tag, load with `new LoadOptions { AutoOrient = false }`. [EXIF orientation in C#](https://recursiveai.org/oneimage/articles/exif-orientation-csharp/) goes into the details.

## Encode JPEG, PNG and WebP with quality

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKBitmap bitmap = SKBitmap.Decode("photo.jpg");
 
using (SKData jpeg = bitmap.Encode(SKEncodedImageFormat.Jpeg, 85))
    File.WriteAllBytes("photo-q85.jpg", jpeg.ToArray());
 
using (SKData webp = bitmap.Encode(SKEncodedImageFormat.Webp, 80))
    File.WriteAllBytes("photo-q80.webp", webp.ToArray());
 
using (SKData png = bitmap.Encode(SKEncodedImageFormat.Png, 100))
    File.WriteAllBytes("photo.png", png.ToArray());
 
// Lossless WebP: SKWebpEncoderOptions go through SKPixmap.Encode.
using (SKPixmap pixmap = bitmap.PeekPixels())
using (SKData? lossless = pixmap.Encode(new SKWebpEncoderOptions(SKWebpEncoderCompression.Lossless, 75)))
    File.WriteAllBytes("photo-lossless.webp", lossless!.ToArray());
 
foreach (string f in new[] { "photo-q85.jpg", "photo-q80.webp", "photo.png", "photo-lossless.webp" })
    Console.WriteLine($"{f}: {new FileInfo(f).Length:N0} bytes");
```

*OneImage · 1.10.1 · .NET 10*

```
using OneImage;
 
Image image = Image.Load("photo.jpg");
 
image.Save("photo-q85.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 85 } });
image.Save("photo-q80.webp", new SaveOptions { EncoderOptions = new WebPEncoderOptions { Quality = 80 } });
image.Save("photo.png");
image.Save("photo-lossless.webp", new SaveOptions { EncoderOptions = new WebPEncoderOptions { Lossless = true } });
 
// Streams have no extension, so name the format.
using var buffer = new MemoryStream();
image.Save(buffer, WebPFormat.Instance);
 
foreach (string f in new[] { "photo-q85.jpg", "photo-q80.webp", "photo.png", "photo-lossless.webp" })
    Console.WriteLine($"{f}: {new FileInfo(f).Length:N0} bytes");
```

From the same 3000×3002 photo:

| Output | SkiaSharp 4.153.1 | OneImage 1.10.1 |
|---|---|---|
| JPEG, quality 85 | 1,279,007 bytes | 1,296,726 bytes |
| WebP, quality 80 | 600,408 bytes | 619,106 bytes |
| PNG | 12,869,113 bytes | 12,873,472 bytes |
| Lossless WebP | 11,739,508 bytes | 11,159,354 bytes |

The two lossless WebP files decode to identical pixels. Quality numbers are each encoder’s own scale, so compare sizes and looks on your images rather than carrying the numbers over. The names map one to one. `SKEncodedImageFormat.Jpeg`, `.Png` and `.Webp` become the file extension, or `JpegFormat.Instance`, `PngFormat.Instance` and `WebPFormat.Instance` for streams. `SKJpegEncoderOptions` and `SKWebpEncoderOptions` become `JpegEncoderOptions` and `WebPEncoderOptions` inside `SaveOptions`. OneImage also writes GIF (animated too), TIFF, BMP and ICO. On save it keeps the ICC profile and EXIF, and corrects the EXIF size and orientation tags to match the saved image. Our [WebP guide](https://recursiveai.org/oneimage/articles/convert-image-to-webp-csharp/) covers the WebP settings.

## Thumbnails

A fast SkiaSharp thumbnail decodes the JPEG at reduced scale with `SKCodec`, then resizes the rest of the way:

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKCodec codec = SKCodec.Create("photo.jpg");
 
// Decode at the smallest scale the JPEG decoder supports that is still at least 400 px wide.
SKSizeI scaled = codec.Info.Size;
foreach (float scale in new[] { 0.125f, 0.25f, 0.5f })
{
    SKSizeI candidate = codec.GetScaledDimensions(scale);
    if (candidate.Width >= 400) { scaled = candidate; break; }
}
 
using var decoded = new SKBitmap(codec.Info.WithSize(scaled.Width, scaled.Height));
codec.GetPixels(decoded.Info, decoded.GetPixels());
 
int height = decoded.Height * 400 / decoded.Width;
using SKBitmap thumb = decoded.Resize(new SKImageInfo(400, height), new SKSamplingOptions(SKCubicResampler.Mitchell));
using SKData webp = thumb.Encode(SKEncodedImageFormat.Webp, 80);
File.WriteAllBytes("thumb.webp", webp.ToArray());
 
Console.WriteLine($"decoded {decoded.Width}x{decoded.Height}, thumbnail {thumb.Width}x{thumb.Height}");
```

*OneImage · 1.10.1 · .NET 10*

```
using OneImage;
 
// Upright, shrink-on-load for JPEG, sRGB, EXIF/XMP/IPTC (and GPS) removed.
Image thumb = Image.Thumbnail("photo.jpg", 400);
thumb.Save("thumb.webp", new SaveOptions { EncoderOptions = new WebPEncoderOptions { Quality = 80 } });
 
Console.WriteLine($"thumbnail {thumb.Width}x{thumb.Height}");
```

SkiaSharp decoded at 750×751 and resized to 400×400, and OneImage wrote 400×400 directly. The loop in the SkiaSharp version is there for a reason. [`GetScaledDimensions`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skcodec.getscaleddimensions) returns a size that “approximately” matches the scale, and asking it for 400 / 3000 returned 375×376, below the target, which then had to be enlarged. `Image.Thumbnail` does the scale choice for you. It also applies EXIF orientation, converts to sRGB, never enlarges unless you pass `upscale: true`, and removes EXIF, XMP and IPTC, so GPS positions don’t leak into public thumbnails. Pass `keepMetadata: true` if you want the metadata kept.

## Drawing shapes, paths and text

Vector drawing is in the core OneImage package, and text is in `RecursiveAI.OneImage.Text`. This draws the same picture both ways:

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKSurface surface = SKSurface.Create(new SKImageInfo(800, 400));
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.White);
 
using var blue = new SKPaint { Color = new SKColor(30, 90, 200), IsAntialias = true };
canvas.DrawCircle(200, 190, 120, blue);
 
using SKPath triangle = SKPath.ParseSvgPathData("M420 300 L540 70 L660 300 Z");
using var red = new SKPaint
{
    Color = new SKColor(200, 30, 30),
    IsAntialias = true,
    Style = SKPaintStyle.Stroke,
    StrokeWidth = 8,
    StrokeJoin = SKStrokeJoin.Round,
};
canvas.DrawPath(triangle, red);
 
using var dashed = new SKPaint
{
    Color = SKColors.Gray,
    IsAntialias = true,
    Style = SKPaintStyle.Stroke,
    StrokeWidth = 4,
    PathEffect = SKPathEffect.CreateDash([12, 4], 0),
};
canvas.DrawLine(40, 340, 760, 340, dashed);
 
using SKTypeface typeface = SKTypeface.FromFile("NotoSans-Regular.ttf");
using var font = new SKFont(typeface, 32);
using var black = new SKPaint { Color = SKColors.Black, IsAntialias = true };
canvas.DrawText("Drawn in C#", 40, 385, SKTextAlign.Left, font, black);   // y is the baseline
 
using SKImage snapshot = surface.Snapshot();
using SKData png = snapshot.Encode(SKEncodedImageFormat.Png, 100);
File.WriteAllBytes("drawing.png", png.ToArray());
```

*OneImage + OneImage.Text · 1.10.1 · .NET 10*

```
using System.Numerics;
using OneImage;
using OneImage.Drawing;
using OneImage.Text;
using Path = OneImage.Drawing.Path;
 
using var canvas = new Canvas(800, 400, PixelFormat.Rgba8);
canvas.Clear(new Rgba32(255, 255, 255, 255));
 
canvas.Fill(new Rgba32(30, 90, 200, 255), Path.Circle(200, 190, 120));
 
Path triangle = Path.ParseSvg("M420 300 L540 70 L660 300 Z");
canvas.Draw(new Pen(Brush.Solid(new Rgba32(200, 30, 30, 255)), 8, StrokeJoin.Round), triangle);
 
var dashed = new Pen(Brush.Solid(new Rgba32(128, 128, 128, 255)), 4, dashPattern: [12, 4]);
canvas.DrawLine(dashed, new Vector2(40, 340), new Vector2(760, 340));
 
var fonts = new FontCollection();
Font font = fonts.Add("NotoSans-Regular.ttf").CreateFont(32);
var options = new TextOptions(font) { Origin = new(40, 385), VerticalAlignment = VerticalAlignment.Bottom };
canvas.DrawText("Drawn in C#", options, new Rgba32(0, 0, 0, 255));
 
canvas.Snapshot().Save("drawing.png");
```

The two 800×400 PNGs show the same picture. Differences are confined to anti-aliased edges and glyph rendering, a mean of 0.65 levels across the image. The text ink sits within 3 pixels of SkiaSharp’s. The calls map like this, with details in the [drawing](https://recursiveai.org/oneimage/docs/drawing/) and [text](https://recursiveai.org/oneimage/docs/text/) docs:

| SkiaSharp | OneImage |
|---|---|
| `SKSurface.Create(new SKImageInfo(w, h))`, `surface.Canvas` | `new Canvas(w, h, PixelFormat.Rgba8)` |
| `canvas.Clear(color)` | `canvas.Clear(color)` |
| `DrawCircle`, `DrawRect`, `DrawPath` with a fill paint | `canvas.Fill(color or brush, Path.Circle(...))`, `Path.Rectangle`, `Path.RoundedRectangle`, `Path.Ellipse`, `Path.Star` |
| `SKPath.ParseSvgPathData`, `SKPath` methods | `Path.ParseSvg`, `PathBuilder` |
| `SKPaintStyle.Stroke`, `StrokeWidth`, `StrokeJoin`, `StrokeCap` | `new Pen(brush, width, join, cap)` with `canvas.Draw(pen, path)` |
| `SKPathEffect.CreateDash(intervals, phase)` | `dashPattern:` and `dashOffset:` on `Pen`, or `Pen.Dash` |
| `SKShader.CreateLinearGradient`, `CreateRadialGradient` | `Brush.LinearGradient`, `Brush.RadialGradient` |
| `canvas.ClipPath(path)` | `clip:` on `Fill` and `Draw`, or `Clip(path, i => ...)` to apply any operation inside a path |
| `SKTypeface.FromFile`, `new SKFont(typeface, size)` | `new FontCollection().Add(path).CreateFont(size)` |
| `canvas.DrawText(text, x, y, align, font, paint)` | `canvas.DrawText(text, new TextOptions(font) { Origin = ... }, color)` |
| `surface.Snapshot()` | `canvas.Snapshot()`, which is O(1) and returns an `Image` |

Three differences to watch for:

- **Anti-aliasing is on by default in OneImage.** In SkiaSharp 4.153.1, a new `SKPaint` has `IsAntialias` set to false. OneImage’s `antialias:` parameter defaults to true.
- **Text is positioned by its box, not by the baseline.** SkiaSharp’s `y` is the baseline. OneImage places the text block relative to `Origin` according to `VerticalAlignment` (default `Top`). The sample uses `Bottom` to land near the same line.
- **Text layout does more.** OneImage.Text wraps at `WrappingLength`, aligns lines, shapes complex scripts (Arabic, Indic and others), runs the Unicode bidirectional algorithm, falls back across font families and draws COLR color glyphs. It reads TrueType, OpenType CFF, collections, WOFF and WOFF2. `SystemFonts` finds installed fonts by scanning the standard directories, without native calls, so it doesn’t depend on the fontconfig library.

Drawing works lazily on an `Image` too. `Image.Load("photo.jpg").Fill(color, path).Save(...)` only computes the tiles the path touches.

## SVG: Svg.Skia to OneImage.Svg

If your SkiaSharp code renders SVG through [Svg.Skia](https://github.com/wieslawsoltes/Svg.Skia) (the sample below follows its README), the OneImage replacement is the `RecursiveAI.OneImage.Svg` package, which registers SVG as an image format:

*Svg.Skia · 5.2.3 · SkiaSharp 4.153.1*

```
using SkiaSharp;
using Svg.Skia;
 
using var svg = new SKSvg();
if (svg.Load("icon.svg") is { } picture)
{
    float scale = 512f / picture.CullRect.Width;
    svg.Save("icon-512.png", SKColors.Empty, SKEncodedImageFormat.Png, 100, scale, scale);
}
```

*OneImage.Svg · 1.10.1 · .NET 10*

```
using OneImage;
using OneImage.Svg;
 
var load = new LoadOptions { Configuration = new ImageConfiguration().Add(SvgFormat.Instance) };
 
// Drawn at 512 px wide, not scaled up from the 24 px natural size.
Image.Load("icon.svg", load).Resize(width: 512).Save("icon-512.png");
```

Both drew our 24×24 icon at 512×512, with a mean difference of 0.09 levels. The [Svg.Skia 5.2.3](https://www.nuget.org/packages/Svg.Skia/5.2.3) listing depends on SkiaSharp and on HarfBuzzSharp with its native assets, so it brings native binaries of its own. OneImage.Svg is managed code. It draws at the requested size instead of scaling up a small render, and it never fetches files or URLs named by the document. See [converting SVG to PNG in C#](https://recursiveai.org/oneimage/articles/svg-to-png-csharp/) and the [SVG docs](https://recursiveai.org/oneimage/docs/svg/) for what it supports.

## PDF pages

SkiaSharp’s [`SKDocument.CreatePdf`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skdocument.createpdf) writes PDF documents. Turning existing PDF pages into images is a different job. In OneImage it’s the `RecursiveAI.OneImage.Pdf` package, which treats each page as a frame of an `Image`:

*OneImage.Pdf · 1.10.1 · .NET 10*

```
using OneImage;
using OneImage.Pdf;
 
var config = new ImageConfiguration().Add(new PdfFormat(new PdfRenderOptions { Dpi = 150 }));
Image pdf = Image.Load("fw9.pdf", new LoadOptions { Configuration = config });   // every page is a frame
 
Image page1 = pdf.Frames[0].ToImage();
page1.Save("page1.png");
page1.Resize(width: 400).Save("page1-thumb.webp");   // drawn at 400 px
 
Console.WriteLine($"{pdf.Frames.Count} pages, page 1 at {page1.Width}x{page1.Height}");
```

On the six-page IRS Form W-9, this printed `6 pages, page 1 at 1275x1650`, which is a US Letter page at 150 dpi. The thumbnail came out 400×518 and was drawn at that size rather than reduced from the full page. OneImage doesn’t write PDF documents, so keep `SKDocument` for that. There’s more in [converting a PDF to an image in C#](https://recursiveai.org/oneimage/articles/pdf-to-image-csharp/) and the [PDF docs](https://recursiveai.org/oneimage/docs/pdf/).

## Pixel access

*SkiaSharp · 4.153.1 · .NET 10*

```
using SkiaSharp;
 
using SKCodec codec = SKCodec.Create("logo.png");
SKImageInfo info = codec.Info.WithColorType(SKColorType.Rgba8888).WithAlphaType(SKAlphaType.Unpremul);
using SKBitmap bitmap = SKBitmap.Decode(codec, info);
 
// 4 bytes per pixel, R G B A; rows may be padded, so step by RowBytes.
Span<byte> pixels = bitmap.GetPixelSpan();
for (int y = 0; y < bitmap.Height; y++)
{
    Span<byte> row = pixels.Slice(y * bitmap.RowBytes, bitmap.Width * 4);
    for (int x = 0; x < row.Length; x += 4)
    {
        byte gray = (byte)((row[x] * 77 + row[x + 1] * 150 + row[x + 2] * 29) >> 8);
        row[x] = row[x + 1] = row[x + 2] = gray;
    }
}
 
using SKData png = bitmap.Encode(SKEncodedImageFormat.Png, 100);
File.WriteAllBytes("logo-gray.png", png.ToArray());
```

*OneImage · 1.10.1 · .NET 10*

```
using OneImage;
 
using Canvas canvas = Image.Load("logo.png").ToCanvas();
 
// T must match the canvas format: this PNG decodes to Rgba8, so T is Rgba32.
using (PixelLock<Rgba32> pixels = canvas.Lock<Rgba32>(new ImageRect(0, 0, canvas.Width, canvas.Height), LockAccess.ReadWrite))
{
    for (int y = 0; y < pixels.Height; y++)
    {
        Span<Rgba32> row = pixels.GetRow(y);
        for (int x = 0; x < row.Length; x++)
        {
            Rgba32 p = row[x];
            byte gray = (byte)((p.R * 77 + p.G * 150 + p.B * 29) >> 8);
            row[x] = new Rgba32(gray, gray, gray, p.A);
        }
    }
}
 
canvas.Snapshot().Save("logo-gray.png");
```

The two grayscale PNGs had identical pixels. In SkiaSharp the byte layout depends on the color type and alpha type. Here, `SKBitmap.Decode("logo.png")` without an `SKImageInfo` returned `Bgra8888` premultiplied, which is why the sample asks for `Rgba8888` unpremultiplied. In OneImage you pick a pixel struct (`Rgba32`, `Rgb24`, `Gray8` and others) that must match the canvas format. `Lock` never converts, and a mismatch throws an exception naming the right struct. `PixelLock.GetRow` hands you one row as a `Span`, so there’s no `RowBytes` arithmetic. For read-only work, `image.ToArray<Rgba32>()` and `CopyPixelsTo` convert from any format. `Image.FromPixels<Rgba32>(pixels, width, height)` goes the other way, replacing `SKBitmap.InstallPixels` or a copy into `GetPixels()`.

## Serving images in ASP.NET Core

SkiaSharp leaves the web side to you, so a resizing endpoint is usually written by hand:

*SkiaSharp · 4.153.1 · ASP.NET Core 10*

```
using SkiaSharp;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
// GET /resize/photo.jpg?width=400 -> WebP, 400 px wide
app.MapGet("/resize/{name}", (string name, int width, IWebHostEnvironment env) =>
{
    if (width is < 1 or > 2000) return Results.BadRequest();
    string file = Path.Combine(env.WebRootPath, "photos", Path.GetFileName(name));
    if (!File.Exists(file)) return Results.NotFound();
 
    using SKBitmap input = SKBitmap.Decode(file);
    int height = Math.Max(1, input.Height * width / input.Width);
    using SKBitmap resized = input.Resize(new SKImageInfo(width, height), new SKSamplingOptions(SKCubicResampler.Mitchell));
    using SKData webp = resized.Encode(SKEncodedImageFormat.Webp, 80);
    return Results.File(webp.ToArray(), "image/webp");
});
 
app.Run();
```

With OneImage, the `RecursiveAI.OneImage.Web` middleware serves any image under `wwwroot` resized, cropped or converted by query string:

*OneImage.Web · 1.10.1 · ASP.NET Core 10*

```
using OneImage.Web;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOneImage(o => o.MaxWidth = 2000);
 
var app = builder.Build();
app.UseOneImage();      // before UseStaticFiles
app.UseStaticFiles();
app.Run();
```

We requested the same photos from both. The hand-written endpoint returned a 400×400 WebP for `/resize/photo.jpg?width=400`, but returned the portrait photo sideways at 400×266, because `SKBitmap.Decode` ignores the EXIF origin. OneImage.Web returned `/photos/photo.jpg?width=400&format=webp&quality=80` as a 400×400 WebP, the portrait upright at 400×600, and `?w=300&h=200&fit=cover` as a 300×200 crop. Both refused `width=5000` with a 400. OneImage.Web also writes a disk cache and sends `ETag`, `Last-Modified` and `Cache-Control: public, max-age=604800`, and it can require signed URLs (`HmacSecretKey`) or named presets. Use one of those on a public site so arbitrary sizes can’t fill the cache. The [OneImage.Web docs](https://recursiveai.org/oneimage/docs/web/) list every command and setting.

## Behavior to expect after porting

1. **Orientation.** Photos load upright. Remove `EncodedOrigin` handling, or the picture will be turned twice.
2. **Sizes.** The computed side is rounded, not truncated, so a few outputs may be one pixel taller or wider than before.
3. **Resize output.** Resampling runs in linear light on premultiplied alpha, and the default filter is Lanczos3 when shrinking. Compare a few outputs, or pass `linear: false` and `filter:` to get closer to your old settings.
4. **Error timing.** `Load` reads only the headers, so damaged pixel data can surface at `Save`. Keep both in the `try`, or load with `new LoadOptions { Validate = true }`. Errors are exceptions (`ImageFormatException` and `ImageLimitException`, both derived from `ImageException`), not `null`. [`SKBitmap.Decode`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skbitmap.decode) returns null on error, and in our test `SKImage.FromEncodedData` and `SKCodec.Create` also returned null for a text file, so delete the null checks and catch the exceptions instead.
5. **Limits.** Size limits are on by default and checked from the headers before anything large is allocated. Very large or suspicious files throw `ImageLimitException`, so raise the limits for trusted inputs that need it.
6. **File lifetime.** `Image.Load(path)` rereads the file when pixels are needed, so leave the file in place until you’ve saved. Load from a `Stream` or `byte[]` to detach from it.
7. **Threads.** An `Image` is safe to read from several threads at once. A `Canvas` has one writer, and you hand `Snapshot()` images to other threads.
8. **Formats.** OneImage reads and writes PNG, JPEG, WebP, GIF, TIFF, BMP and ICO, with add-on packages for SVG, PDF and Photoshop files. Check your inputs against that list.

## What SkiaSharp has that OneImage doesn’t

OneImage covers server-side image work: decoding, resizing, editing, drawing, text and encoding. If you depend on any of the following, keep SkiaSharp for that part:

- **GPU rendering.** SkiaSharp draws through OpenGL, Vulkan, Direct3D and Metal via [`GRContext`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.grcontext). OneImage renders on the CPU.
- **UI integration.** SkiaSharp has view packages for app frameworks, such as [SkiaSharp.Views.Maui.Controls](https://www.nuget.org/packages/SkiaSharp.Views.Maui.Controls/4.153.1). OneImage has no UI controls.
- **Vector document output.** SkiaSharp writes PDF and XPS through `SKDocument` and SVG through [`SKSvgCanvas`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.sksvgcanvas). OneImage reads SVG and PDF but writes only raster formats.
- **Runtime shaders.** [`SKRuntimeEffect`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skruntimeeffect) compiles shader, color filter and blender programs at run time. OneImage has no shader language. Custom brushes are C# classes.
- **Older and mobile targets.** The [SkiaSharp 4.153.1](https://www.nuget.org/packages/SkiaSharp/4.153.1) listing includes .NET Framework 4.6.2 and 4.8, .NET Standard 2.0 and 2.1, and Android, iOS, Mac Catalyst, tvOS and Tizen. OneImage runs on `net10.0`.
- **Some speed.** In OneImage’s [published benchmarks](https://recursiveai.org/oneimage/docs/performance/) (AMD Ryzen 9 7950X3D, .NET 10; the benchmark project references SkiaSharp 4.153.1), SkiaSharp decodes a 50-frame 500×500 GIF in 1.0 ms against OneImage’s 2.3 ms, and encodes a 2000×1500 lossless WebP in 101.4 ms against 146.8 ms. The same table has OneImage ahead on PNG, lossy and lossless WebP, BMP and JPEG-to-RGBA decoding.
- **Maturity.** SkiaSharp wraps Google’s Skia graphics library. OneImage is new.

The project’s [comparison page](https://recursiveai.org/oneimage/docs/comparison/) sets OneImage against SkiaSharp, ImageSharp, Magick.NET and NetVips. The [migration reference](https://recursiveai.org/oneimage/docs/migrating/) covers more of the API side by side. If you’re also moving off ImageSharp, see [migrating from ImageSharp to OneImage](https://recursiveai.org/oneimage/articles/migrate-from-imagesharp/). For SkiaSharp’s licensing, see our [SkiaSharp license guide](https://recursiveai.org/oneimage/articles/skiasharp-license/).
