Recursive AI

Getting started

All oneimage docs

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

Terminal
dotnet add package RecursiveAI.OneImage

The 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

C#
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

C#
// 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

C#
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.

C#
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.

Terminal
dotnet add package RecursiveAI.OneImage.Web
C#
using 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:

Text
/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):

C#
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:

PackageWhat it doesInstall
OneImage.WebASP.NET Core middleware: URL-driven resizing, cropping and conversion, a disk cache, signed URLs and request limits.dotnet add package RecursiveAI.OneImage.Web
OneImage.TextFonts 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.SvgSVG and SVGZ as an image format, drawn at the size you ask for. Depends on OneImage.Text.dotnet add package RecursiveAI.OneImage.Svg
OneImage.PsdPSD 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.PdfPDF 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:

C#
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:

C#
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

Guides: compress images, grayscale, watermarks, animated GIFs, multipage TIFF, PDF to TIFF, PSD to PNG and resizing on the fly in ASP.NET Core.