OneImage is an image library for .NET written in pure C#, with no dependency beyond .NET itself. This page goes from install to a first thumbnail, format conversion, editing, and serving resized images from ASP.NET Core. Every sample on it was compiled and run against the 1.10.1 packages from NuGet.
Install
OneImage targets .NET 10 (net10.0).
dotnet add package RecursiveAI.OneImageThe package is RecursiveAI.OneImage and the namespace is OneImage. The package has no dependencies: no native binaries, no other NuGet packages. NativeAOT and trimming are supported.
Your first thumbnail
using OneImage; // 400 px wide, upright, in sRGB, EXIF and GPS removed, written as WebP. Image.Thumbnail("in.jpg", 400).Save("thumb.webp");
Image.Thumbnail does what a web-ready copy needs, in this order: it applies the EXIF orientation, resizes (a JPEG is decoded at 1/2, 1/4 or 1/8 scale when that is enough), converts to sRGB and strips EXIF, XMP and IPTC. It never makes an image larger than the original unless you pass upscale: true. It also takes a Stream, a ReadOnlyMemory<byte> or an existing Image.
Convert format and quality
// The output format follows the file extension. Image.Load("in.jpg").Save("out.png"); // Quality and other codec settings go in SaveOptions. Image.Load("in.png").Save("out.jpg", new SaveOptions { EncoderOptions = new JpegEncoderOptions { Quality = 85 } }); Image.Load("in.png").Save("out.webp", new SaveOptions { EncoderOptions = new WebPEncoderOptions { Quality = 80 } }); // A stream has no extension, so name the format. using var stream = File.Create("out-stream.png"); Image.Load("in.jpg").Save(stream, PngFormat.Instance);
JPEG and WebP default to quality 75. Every format's options (PNG compression level, WebP lossless, GIF palette size, TIFF compression) are in the guide. The core package reads and writes PNG, JPEG, WebP, GIF, TIFF, BMP and ICO, and detects the format from the content, not the extension.
Load, edit, save
Image photo = Image.Load("in.jpg"); // reads the headers only Console.WriteLine($"{photo.Width} x {photo.Height}"); // Fill a 200 x 200 box, cropping around the busiest part. photo.Resize(width: 200, height: 200, fit: Fit.Cover, anchor: Anchor.Smart) .Save("square.jpg"); // Each call returns a new Image; photo itself never changes. photo.Crop(0, 0, 1200, 800) .Rotate(90) .Flip(FlipAxis.Horizontal) .Save("edited.png");
Resize takes a Fit (Contain, Cover, Stretch, Pad, BoxPad, Min) and an Anchor; leave out width or height to keep the aspect ratio. Crops, rotations, padding, color adjustments, blurs and filters, compositing and pixel access through a Canvas are in the guide.
The lazy model
An Image is a description of work, not a decoded bitmap: Load reads only the headers, each operation returns a new immutable Image, and pixels are computed tile by tile when you Save. So a file whose header is fine but whose data is damaged loads without error and throws at Save: put the Save inside your try, or load with Validate = true.
try { Image.Load("upload.jpg").Resize(width: 800).Save("upload.webp"); } catch (ImageException ex) // damaged data, a size limit, an unsupported operation { Console.WriteLine(ex.Message); } // To fail at Load instead, decode once up front. Image upload = Image.Load("upload.jpg", new LoadOptions { Validate = true });
All of OneImage's own exceptions derive from ImageException. Because an image loaded from a path reopens the file when its pixels are needed, keep the file in place until you are done, or load from a Stream or byte[] instead (sources and lifetime).
Serve images from ASP.NET Core
RecursiveAI.OneImage.Web resizes, crops and converts images on request, driven by the query string, and caches the results on disk. It is a separate package, so the core package stays free of ASP.NET Core.
dotnet add package RecursiveAI.OneImage.Webusing OneImage.Web; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOneImage(); var app = builder.Build(); app.UseOneImage(); // before UseStaticFiles app.UseStaticFiles(); app.Run();
Every image under wwwroot can now be asked for in another size or format:
/photos/cat.jpg?width=400 400 px wide, same format, aspect ratio kept /photos/cat.jpg?w=300&h=300&fit=cover 300 x 300, cropped around the center /photos/cat.jpg?width=800&format=webp&q=80 WebP at quality 80
A request without commands falls through to the static file middleware untouched, and an invalid value such as width=abc is a 400, never silently clamped. Requests for the same result are processed once and then served from the cache, with ETag, Last-Modified and 304 responses.
On a public site, sign your URLs or allow only presets, because every distinct valid set of commands creates a cache entry. With a key set, a request without a valid hmac gets a 403. Here the key is at least 32 random bytes, base64, from configuration (for example the environment variable Images__Key):
using OneImage.Web; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOneImage(o => { o.MaxWidth = 2000; o.Presets["thumb"] = "width=200&height=200&fit=cover&format=webp"; o.HmacSecretKey = Convert.FromBase64String(builder.Configuration["Images:Key"]!); }) .ConfigureCache(c => c.CacheRootPath = "/var/cache/images"); var app = builder.Build(); app.UseOneImage(); app.UseStaticFiles(); var signer = app.Services.GetRequiredService<ImageUrlSigner>(); string src = signer.Sign("/photos/cat.jpg?preset=thumb"); // adds &hmac=... app.MapGet("/", () => Results.Text($"<img src=\"{src}\">", "text/html")); app.Run();
The defaults assume a public, unauthenticated endpoint: output is at most 4096 px a side and 16,777,216 pixels, sources at most 64 MiB, queries at most 1024 characters, at most one image per processor (at least two) is processed at a time, EXIF, XMP and IPTC are removed, and the cache folder sits outside wwwroot. The ImageSharp.Web query spellings (rmode, ranchor, rsampler, bgcolor and the rest) are accepted too. Commands, providers, hooks and the cache layout are in OneImage.Web.
Add-on packages
Drawing (paths, fills, strokes, brushes, clipping, boolean operations) and layered documents are in the core package. The rest comes as separate packages, so you only ship what you use:
| Package | What it does | Install |
|---|---|---|
| OneImage.Web | ASP.NET Core middleware: URL-driven resizing, cropping and conversion, a disk cache, signed URLs and request limits. | dotnet add package RecursiveAI.OneImage.Web |
| OneImage.Text | Fonts and text: OpenType shaping, right-to-left and complex scripts, line breaking, rich text and color glyphs. Depends on RecursiveAI.OneFont. | dotnet add package RecursiveAI.OneImage.Text |
| OneImage.Svg | SVG and SVGZ as an image format, drawn at the size you ask for. Depends on OneImage.Text. | dotnet add package RecursiveAI.OneImage.Svg |
| OneImage.Psd | PSD and PSB: the merged image as an image format, and the layers as a document you can edit and write back. | dotnet add package RecursiveAI.OneImage.Psd |
| OneImage.Pdf | PDF pages as an image format, each page a frame, drawn at the size you ask for. Depends on RecursiveAI.OnePdf. | dotnet add package RecursiveAI.OneImage.Pdf |
SVG, PSD and PDF are not in the default configuration. Register the ones you use, then load them like any other file:
using OneImage; using OneImage.Pdf; using OneImage.Psd; using OneImage.Svg; var config = new ImageConfiguration() .Add(SvgFormat.Instance) .Add(PsdFormat.Instance) .Add(new PdfFormat(new PdfRenderOptions { Dpi = 150 })); var load = new LoadOptions { Configuration = config }; // SVG is drawn at the size you ask for, not scaled up. Image.Load("logo.svg", load).Resize(width: 400).Save("logo.png"); // PSD loads as its merged image. Image.Load("poster.psd", load).Resize(width: 800).Save("poster.webp"); // Each PDF page is a frame. Image.Load("report.pdf", load).Frames[0].ToImage().Save("page1.png");
Text is drawn through the same vector drawing as shapes:
using OneImage; using OneImage.Drawing; using OneImage.Text; using Path = OneImage.Drawing.Path; var fonts = new FontCollection(); Font font = fonts.Add("DejaVuSans.ttf").CreateFont(48); Image.Load("in.jpg") .Fill(new Rgba32(255, 255, 255, 180), Path.RoundedRectangle(40, 40, 520, 120, 16)) .DrawText("Hello from OneImage", new TextOptions(font) { Origin = new(70, 115) }, Brush.Solid(new Rgba32(20, 20, 20, 255))) .Save("captioned.jpg");
Where next
- Guide: the lazy model, errors, operations,
Canvas, metadata, color, animation, limits, threading, AOT and trimming. - OneImage.Web: every command, the security defaults, signed URLs, caching and providers.
- Drawing, Text and Layers.
- Migrating from ImageSharp and System.Drawing, with code side by side, and migrating from SkiaSharp.
- Comparison with ImageSharp, SkiaSharp, Magick.NET and NetVips, and the benchmarks.
- All docs, or all of them as one Markdown file for an LLM: llms-full.txt.
Guides: compress images, grayscale, watermarks, animated GIFs, multipage TIFF, PDF to TIFF, PSD to PNG and resizing on the fly in ASP.NET Core.