loading

Getting started

Bmage is browser-side image processing for Blazor: load, transform, annotate and export images on the user's device from strongly typed C#. Pick a package, register the services, and an image is three calls away.

Pick a package

Each package includes the ones below it, so install the top one you need.

PackageUse it whenDepends on
Bit.Bmage.EditorYou want the BmageEditor componentBit.Bmage
Bit.BmageYou want browser codecs, loading and export, but your own UIBit.Bmage.Core
Bit.Bmage.CoreYou want the engine with no Blazor dependency at allnothing
Shell
dotnet add package Bit.Bmage.Editor

All three target .NET 8, 9 and 10. Everything works on Blazor WebAssembly and on Interactive Auto once interactive; Interactive Server works too, with pixel data crossing the circuit. Hosting models covers each.

Register the services

In Program.cs of the project that runs in the browser. For a Blazor Web App using WebAssembly or Auto render modes that is the client project - and the server project too when it prerenders, or when any component uses Interactive Server.

C#
using Bit.Bmage;   // AddBitBmageServices and AddBitBmageHeadlessServices live here​builder.Services.AddBitBmageServices();​// Or, with options:builder.Services.AddBitBmageServices(options =>{    // Refuse anything larger than this before decoding, to protect against decompression bombs.    options.Limits = ImageLimits.Default with { MaxPixels = 40_000_000 };​    // Largest upload accepted from a file input.    options.MaxSourceBytes = 32 * 1024 * 1024;​    // Longest edge used for interactive editing; larger images edit through a proxy.    options.MaxInteractiveDimension = 2048;});

This registers ImageLoader, ImageExporter, IImageCapabilityProvider and ImageProcessorFactory. It does not register ImageProcessor itself: in a browser the processor can only be built once the browser's codec support has been probed, which is asynchronous - so you ask the factory for one when you need the lower-level API.

Razor

Services are scoped. In WebAssembly a scope lasts for the whole application; in Server there is one per circuit, so browser state is never shared between users.

Add the stylesheet

Only Bit.Bmage.Editor needs one. Link it from App.razor in a Blazor Web App, or from wwwroot/index.html in a standalone WebAssembly app. The browser bridge script is imported on demand by the library - there is no script tag to add.

HTML
<link rel="stylesheet" href="_content/Bit.Bmage.Editor/bit-bmage-editor.css" />

Your first image

Decode with the browser's codecs (honouring the phone's EXIF rotation), process the decoded pixels, and encode - preferring WebP, falling back where it is unavailable.

Razor
@page "/upload"@inject ImageLoader Loader@inject ImageExporter Exporter​<InputFile OnChange="OnChange" accept="image/*" />​@if (_preview is not null){    <img src="@_preview" alt="Processed preview" />    <p>@_size bytes</p>}​@code {    private string? _preview;    private long _size;​    private async Task OnChange(InputFileChangeEventArgs e)    {        // Decode with the browser's codecs, honouring the phone's EXIF rotation.        using var image = await Loader.LoadAsync(e.File);​        // Process on the decoded pixels.        using var processed = await ImagePipeline.Create()            .MaxSize(800, 800)            .Sharpen(0.3f)            .ExecuteAsync(image);​        // Encode, preferring WebP but falling back where it is unavailable.        var encoded = await Exporter.EncodeWithFallbackAsync(            processed,            [ImageFormat.WebP, ImageFormat.Jpeg],            new ImageExportOptions { Quality = 0.85 });​        _size = encoded.Length;        _preview = await Exporter.CreateObjectUrlAsync(encoded);    }}

Run it here

The same pipeline, run in your browser against a generated 1200 × 800 scene - no upload involved.

Two things that are easy to miss

Dispose your images. An ImageBuffer holds pooled memory: a 4000 × 3000 image is 48 MB. using returns it to the pool immediately instead of waiting for a collection. Buffers you receive from LoadAsync, ExecuteAsync and RenderFullAsync are yours to dispose; buffers exposed as properties, such as ImageEditSession.Preview, are not.

Prefer object URLs to data URLs. CreateObjectUrlAsync hands the browser the bytes directly; base64 inflates them by a third and forces them through the DOM as a string. Release object URLs with ReleaseObjectUrlAsync, or dispose the exporter, which releases every URL it created.

The full editor instead

If what you want is an editing UI rather than a pipeline, the editor is one component. It fills its container, and a container with no height collapses to nothing - so give it one.

Razor
@page "/edit"​<div style="height: 70vh">    <BmageEditor @ref="_editor" OnExported="OnExported" /></div>​<button @onclick="Save">Save</button>​@code {    private BmageEditor? _editor;​    private Task Save() => _editor!.DownloadAsync("edited",        new ImageExportOptions { Format = ImageFormat.WebP, Quality = 0.9 });​    private void OnExported(EncodedImage image)    {        // Also called when the user exports from the editor's own panel.    }}

The editor page has it running, with theming, localisation and the full parameter reference.

When things run

Browser features need a live browser. During prerendering there is not one yet, so call them from OnAfterRenderAsync or an event handler rather than from OnInitializedAsync.

C#
protected override async Task OnAfterRenderAsync(bool firstRender){    if (!firstRender) return;    _capabilities = await Capabilities.GetAsync();    StateHasChanged();}

IImageCapabilityProvider.GetAsync is deliberately forgiving here: with no browser reachable it returns the managed capability set with Environment set to Prerendering rather than throwing, so a prerendered page still renders something sensible and detects properly once interactive. Every page on this site is prerendered that way.

An unhandled error has occurred. Reload 🗙