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.
| Package | Use it when | Depends on |
|---|---|---|
Bit.Bmage.Editor | You want the BmageEditor component | Bit.Bmage |
Bit.Bmage | You want browser codecs, loading and export, but your own UI | Bit.Bmage.Core |
Bit.Bmage.Core | You want the engine with no Blazor dependency at all | nothing |
dotnet add package Bit.Bmage.EditorAll 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.
using Bit.Bmage; // AddBitBmageServices and AddBitBmageHeadlessServices live herebuilder.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.
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.
<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.
@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.
@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.
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.