loading

Pipelines

An ImagePipeline is an immutable, ordered list of operations. Building one does no work: it is a description of an edit that you can inspect, store, reuse across images and threads, and execute when you are ready.

Build one

Every choice below is a builder call. Each returns a new pipeline and leaves the old one as it was - which is what makes a pipeline safe to keep in a static field and reuse for every upload.

A generated 1200 × 800 scene; every change re-runs the pipeline.

Pipeline

Auto orient → Resize(600x600, Max) → Vibrance → Sharpen
Output size 600 × 400
Steps run 4 of 4
Took —

After the optimiser

Auto orient → Resize(600x600, Max) → Vibrance → Sharpen

Geometry


Adjustments and filters


Brightness(0) and Contrast(0) do nothing, and the optimiser drops them - watch the step count.

Building and inspecting

C#
var pipeline = ImagePipeline.Create()    .AutoOrient()    .MaxSize(1920, 1920)    .Sharpen(0.3f);​Console.WriteLine(pipeline);// Auto orient → Resize(1920x1920, Max) → Sharpen​Console.WriteLine(pipeline.GetOutputSize(new Size(4000, 3000)));// {Width=1920, Height=1440}   ← computed without touching a single pixel

Executing

C#
using var result = await pipeline.ExecuteAsync(source, new PipelineExecutionOptions{    CancellationToken = token,    Progress = new Progress<double>(p => _percent = p),});

The source is never modified, and the returned buffer is yours to dispose. ExecuteAsync yields to the scheduler between operations so a single-threaded WebAssembly UI can repaint; Execute is the synchronous form, for use off the UI thread or on small images.

OptionMeaning
AllocatorWhere intermediate buffers come from. Defaults to the shared pool.
LimitsSafety limits applied to every buffer allocated during the run.
CancellationTokenChecked between operations and periodically inside them, typically every few rows.
ProgressReceives 0 to 1 across the whole run, with each operation's own progress mapped into its slice.
OptimizeRun the optimiser first. Default true.
YieldBetweenOperationsYield to the scheduler between steps. Default true.
YieldCallbackHow to yield, if the default Task.Yield is not right for your host.

Failures are wrapped with context rather than surfacing bare - an ImageException names the operation and its step, such as Operation 'Crop' (step 2 of 4) failed: Crop rectangle ... does not intersect the image (800x600).

Geometry

C#
pipeline.AutoOrient()                       // apply the EXIF rotation so the image is upright        .Crop(x, y, width, height)        .CropToAspect(AspectRatio.Ratio16x9, Anchor.Center)        .Resize(width, height, mode, filter, anchor, background)        .MaxSize(1920, 1080)                // shrink to fit, never enlarge        .Cover(512, 512, Anchor.Top)        // fill exactly, cropping the overflow        .Rotate(12.5, background)           // arbitrary angle, canvas expands        .Orient(Orientation.Rotate90)       // lossless, no resampling        .FlipHorizontal()        .Skew(10, 0);

AutoOrient is almost always the first thing you want. Phones record rotation in EXIF rather than rotating the pixels, so an image that looks right in a photo viewer will appear sideways on a canvas unless it is applied. The browser decoder applies it during decoding; AutoOrient handles the cases where it has not been.

The six resize modes

For a 1000 × 500 source asked to fit 400 × 400:

ModeResultBehaviour
Fit400 × 200Scale proportionally to fit inside. The default.
Contain400 × 400Like Fit, then pad to exactly the target with background.
Cover400 × 400Scale to fill, then crop the overflow according to anchor.
Stretch400 × 400Ignore the aspect ratio.
Max400 × 200Like Fit, but never enlarge a smaller image - the right choice for an upload cap.
Min800 × 400Scale so both dimensions are at least the target.

Resampling filters

FilterUse it for
AutoThe default. Lanczos3 for downscaling, bicubic for upscaling.
BoxFast area averaging; excellent for large reductions.
BilinearFast, moderate quality.
BicubicGood general purpose quality.
Lanczos3Sharpest downscaling, slowest.
NearestNeighborPixel art, or wherever hard edges must survive.

Large reductions automatically get an exact integer box prepass before the chosen filter, which is why a 24 megapixel thumbnail takes 52 ms rather than 368 ms. It is transparent and quality-neutral.

Smart cropping

A fixed anchor is wrong most of the time: centre-cropping a portrait to a square routinely cuts off the top of a head. SmartCrop scores candidate positions on a small downscaled copy using local detail, colour saturation and skin-like hue, then picks the best. It is a heuristic, not face recognition - when you do have a detector, hand it the rectangle. The Avatar page compares it with the fixed anchors.

C#
pipeline.SmartCrop(AspectRatio.Square);​// With a face detector of your own:var crop = SmartCrop.FindCrop(image, AspectRatio.Square, mustInclude: faceRectangle);pipeline.Crop(crop.Rectangle);

Adjustments and filters

C#
pipeline.Brightness(0.1f).Contrast(0.15f).Saturation(-0.2f).Hue(15)        .Exposure(0.5f).Gamma(1.2f).Temperature(0.2f).Tint(-0.1f)        .Vibrance(0.3f).ShadowsHighlights(0.2f, -0.15f).Opacity(0.9f)        .Grayscale().Sepia(0.8f).Invert().Posterize(6).Threshold(0.5f)        .Blur(4f).Sharpen(0.5f).Pixelate(12).Noise(0.05f)        .DenoiseMedian(1).Vignette(0.4f)        .ColorMatrix(myMatrix);​pipeline.AutoLevels();                    // stretch the tonal rangepipeline.AutoLevels(perChannel: true);    // also neutralise a colour castpipeline.AutoContrast(targetMean: 0.5f);  // gentler: move the mean luminance towards a target​// A region confines a filter - this is how redaction works.pipeline.Pixelate(14, new Rectangle(40, 120, 300, 40));

Amounts are consistently in [-1, 1] with 0 meaning no change, except where another unit is natural: Exposure is in stops, Hue in degrees, Gamma is a positive multiplier and Blur a radius in pixels. Filters that take a region only touch that region, which is how redaction works.

AutoLevels stretches the tonal range - the most useful single fix - ignoring the darkest and brightest 0.5% by default, so one hot pixel cannot define the white point, and it leaves a flat histogram alone rather than amplifying it into noise.

The optimiser

It runs by default, and every rewrite is exact.

RuleEffect
Drop identitiesBrightness(0) and friends disappear.
Merge cropsTwo crops become one.
Merge orientationsRotate 90 twice becomes rotate 180; two horizontal flips vanish entirely.
Fuse resize with a following cropOne resample that computes only the surviving region, 2.4× faster.
Fuse per-pixel runsOne pass over the pixels instead of one per adjustment.
C#
// Three no-ops and two crops.var pipeline = ImagePipeline.Create()    .Brightness(0)    .Crop(10, 10, 200, 200)    .Crop(5, 5, 100, 100)    .Contrast(0);​pipeline.Optimize(new Size(400, 400));// Crop(15,15,100,100)

Fusing per-pixel operations removes the intermediate 8-bit rounding that separate execution performs, so a fused chain is more accurate, not merely faster. Turn the optimiser off with new PipelineExecutionOptions { Optimize = false } if you are comparing behaviour, but there is no reason to in production.

Previewing at another resolution

ForScale rescales every operation - crop rectangles, blur radii, pixelation block sizes, stroke widths, font sizes. This is how the editor previews full-resolution edits on a proxy and gets the same picture, and why there is one rendering path rather than a fast one and an accurate one that drift apart.

C#
var preview = pipeline.ForScale(0.25);

Custom operations

Implement IImageOperation, or derive from PointOperation if your operation maps each pixel independently - then it joins the single-pass run with the adjustments around it for free.

C#
An unhandled error has occurred. Reload 🗙