A browser app installs Bit.Bmage.Blazor: the browser codecs, loading and export, and the editor component. It brings
the engine, Bit.Bmage, along, and otherwise depends only on Microsoft.AspNetCore.Components.Web.
dotnet add package Bit.Bmage.BlazorA server, a console tool or a worker that only processes images installs Bit.Bmage alone, which has no dependency on
Blazor; see headless processing.
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. Register it in the server project as well whenever the server instantiates the same
components: during prerendering (on by default) and for any component that uses Interactive Server. In a Blazor Hybrid
app (MAUI, WPF, Windows Forms) there is one container: register it once, in MauiProgram.cs or its equivalent, next
to AddMauiBlazorWebView() (see Blazor Hybrid).
using Bit.Bmage; // AddBitBmageServices and AddBitBmageHeadlessServices live here
builder.Services.AddBitBmageServices();This registers ImageLoader, ImageExporter, IImageCapabilityProvider and ImageProcessorFactory. It does not
register ImageProcessor directly, because in a browser the processor can only be built once the browser's codec
support has been probed, and that probe is asynchronous. Get one from the factory when you need the lower level API:
@inject ImageProcessorFactory ProcessorFactory
// Inside an event handler or OnAfterRenderAsync, never during prerendering.
var processor = await ProcessorFactory.GetAsync();AddBitBmageHeadlessServices has no such constraint and does register ImageProcessor directly, because the managed
codecs need no probing. That is the one difference in the service graph between the two registrations.
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;
// Threads an operation may use: one by default. Environment.ProcessorCount uses every core, in the browser only
// when the app is built with threads.
options.MaxDegreeOfParallelism = null;
// Web Workers the browser codecs may use; 0 keeps decoding and encoding on the page's thread.
options.MaxCodecWorkers = null;
});Services are registered as scoped. In WebAssembly that scope lasts for the whole application; in Server there is one scope per circuit, so browser state is never shared between users; in Blazor Hybrid there is one per BlazorWebView.
BmageEditor also works without the registration, so a WebAssembly app can download Bmage only when someone opens the
editor. When ImageLoader is not registered, the editor builds every service it needs itself, once per browser
connection, with the BitBmageOptions the app registered or the defaults. Mark both assemblies as lazy loaded and load
them before the editor first renders:
<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="Bit.Bmage.wasm" />
<BlazorWebAssemblyLazyLoad Include="Bit.Bmage.Blazor.wasm" />
</ItemGroup>@inject LazyAssemblyLoader LazyAssemblyLoader
await LazyAssemblyLoader.LoadAssembliesAsync(["Bit.Bmage.wasm", "Bit.Bmage.Blazor.wasm"]);Leave AddBitBmageServices out of Program.cs in that project: calling it references Bmage's types before the
assemblies are there. Code of your own that injects ImageLoader or ImageExporter needs the registration, so lazy
loading suits an app whose only use of Bmage is the editor. Do not lazy load in an AOT build: AOT compiles every
assembly into dotnet.native.wasm, so there is nothing left to download later, and lazy loading an AOT-compiled
assembly can fail at startup.
In a Blazor Web App only the browser lazy loads. Give Routes the InteractiveWebAssembly or InteractiveAuto
render mode in App.razor, so the router - and its OnNavigateAsync - runs in the browser; with per-page render
modes it renders statically on the server and never loads the assemblies in the browser. The server project loads
Bmage with the rest of the app and keeps its AddBitBmageServices(), and registers LazyAssemblyLoader, which the
router's file injects while it is prerendered there and which loads nothing outside the browser:
builder.Services.AddScoped<LazyAssemblyLoader>().
The lazy loading sample
does all of this in a Blazor Web App - its project file leaves both marks out of AOT builds, and sets a switch next to
them that tells its router whether there is anything to load; its server, which prerenders the editor's page, keeps
AddBitBmageServices() and registers LazyAssemblyLoader, which the router injects there too - and the
end-to-end suite
runs its published copy in a browser, trimmed and AOT-compiled: the home page downloads neither assembly, and the
editor's page - opened from the home page or by a deep link the server prerenders - downloads each once, and its
editor opens, turns and exports an image with nothing registered in the browser.
Only needed for the editor component (BmageEditor, BmageDropZone). Put it in App.razor of a Blazor Web App, or
in wwwroot/index.html of a standalone WebAssembly or Blazor Hybrid app:
<link rel="stylesheet" href="_content/Bit.Bmage.Blazor/bit-bmage-editor.css" />The browser bridge script is loaded on demand by the library. You do not add a script tag for it.
For WebAssembly, add this to the project that runs in the browser:
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>Bmage is tuned for Blazor's default interpreter, and the browser does the work it is fastest at, but what stays in .NET
still runs 4 to 12× faster compiled ahead of time. AOT needs the wasm-tools workload and only takes effect on
dotnet publish, so a page that feels slow under dotnet run may be fine once published. performance.md has the measurements and the
trade-offs.
@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);
}
}Two things in that snippet matter and 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. ToDataUrl() exists
but base64 inflates the payload by a third and forces it through the DOM as a string. Release object URLs with
ReleaseObjectUrlAsync, or dispose the exporter, which releases every URL it created.
@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 needs a height. It fills its container, and a container with no height collapses to nothing.
The same engine runs on a server or in a console application with no JavaScript at all. Only the managed codecs are available there: PNG, JPEG, BMP, GIF, WebP and AVIF for decoding, PNG, JPEG, BMP and WebP for encoding.
using Bit.Bmage;
builder.Services.AddBitBmageHeadlessServices();app.MapPost("/upload", async (IFormFile file, ImageProcessor processor) =>
{
using var buffer = new MemoryStream();
await file.OpenReadStream().CopyToAsync(buffer);
var thumbnail = await processor.ProcessAsync(
buffer.ToArray(),
ImagePipeline.Create().AutoOrient().MaxSize(400, 400),
new ImageExportOptions { Format = ImageFormat.Jpeg, Quality = 0.8 });
return Results.File(thumbnail.Data, thumbnail.MimeType);
});Browser features need a live browser. During prerendering there is not one yet, so calling them from
OnInitializedAsync on the server will fail. Call them from OnAfterRenderAsync or from an event handler:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
_capabilities = await Capabilities.GetAsync();
StateHasChanged();
}IImageCapabilityProvider.GetAsync is deliberately forgiving here: when no browser is reachable it returns the managed
capability set with Environment set to Prerendering rather than throwing, so a prerendered page still renders
something sensible and re-detects once interactive.
- Pipelines covers composing and optimising operations.
- The editor component covers theming, localisation and replacing parts of the UI.
- Formats and capabilities covers what each environment decodes and encodes, and what to do when a browser cannot write a format.
- Metadata and privacy covers EXIF, GPS and the three metadata policies.