Hosting models
Bmage's engine is C# that runs wherever .NET does, and its browser integration runs wherever Blazor has a browser to talk to. What changes between hosts is where the pixels live - and, in a prerendered app, when there is a browser at all.
What works where
| Model | What works | What to know |
|---|---|---|
| Blazor WebAssembly | Everything | The intended home. Processing runs on the user's device with no network involved. |
Blazor Web App, InteractiveWebAssembly | Everything | Same as above once the component is interactive. This site. |
Blazor Web App, InteractiveAuto | Everything once interactive | The first visit runs on the server while the runtime downloads - see Interactive Server. |
Blazor Web App, InteractiveServer | Everything | Works, but pixels cross the circuit. |
| Prerendering | Managed codecs only | No browser exists yet. Call browser features from OnAfterRenderAsync. |
| ASP.NET Core, console, worker | Headless API | AddBitBmageHeadlessServices. Managed codecs only, no JavaScript. |
In a Blazor Web App
A Web App has two projects and both build a service container: the client's runs in the browser, and the server's prerenders every page before the runtime has downloaded. Every component is instantiated in both, so both have to register Bmage - or the prerender fails with a missing-service exception. This site keeps the registration in one extension method in the client project and calls it from both.
The client project
Reference Bit.Bmage.Editor (or Bit.Bmage) from the client, which is
the project that runs in the browser.
// Client/Extensions/IServiceCollectionExtensions.csusing Bit.Bmage;namespace Microsoft.Extensions.DependencyInjection;public static class IServiceCollectionExtensions{ // Called by the client's Program.cs and by the server's, for the prerender pass. public static IServiceCollection AddDemoServices(this IServiceCollection services) { services.AddBitBmageServices(options => { options.MaxSourceBytes = 64 * 1024 * 1024; options.Limits = ImageLimits.Default with { MaxPixels = 80_000_000 }; }); return services; }}The server project
The server references the client project, registers the same services for its prerender pass, and maps the client's assembly so its pages are routable.
// Server/Program.csvar builder = WebApplication.CreateBuilder(args);builder.Services.AddRazorComponents() .AddInteractiveWebAssemblyComponents();// The prerender pass instantiates the client's components in this container,// so it has to register the very same services the WebAssembly container does.builder.Services.AddDemoServices();var app = builder.Build();app.UseAntiforgery();app.MapStaticAssets();app.MapRazorComponents<App>() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);app.Run();App.razor
The editor's stylesheet goes in the head, and the app renders in
InteractiveWebAssembly (or InteractiveAuto), so the image work
happens on the user's device.
<head> ... <link rel="stylesheet" href="_content/Bit.Bmage.Editor/bit-bmage-editor.css" /> <HeadOutlet @rendermode="InteractiveWebAssembly" /></head><body> <Routes @rendermode="InteractiveWebAssembly" /> <script src="_framework/blazor.web.js"></script></body>Prerendering
During prerendering there is no browser, so IJSRuntime cannot be used. Rather
than throwing, IImageCapabilityProvider.GetAsync returns the managed capability
set with Environment set to Prerendering, so a prerendered page still
renders something sensible and detects properly once interactive. Move browser work to
OnAfterRenderAsync or an event handler.
@inject IImageCapabilityProvider Capabilities@if (_capabilities is null){ <p>Probing this browser...</p>}@code { private ImageCapabilities? _capabilities; protected override async Task OnAfterRenderAsync(bool firstRender) { // OnAfterRenderAsync never runs during prerendering, and RendererInfo says // whether this render is the interactive one. if (!firstRender || !RendererInfo.IsInteractive) return; _capabilities = await Capabilities.GetAsync(); StateHasChanged(); }}
Buttons on a prerendered page are visible before the runtime that answers them has loaded. This
site disables its own until RendererInfo.IsInteractive is true, and the header
says loading in the meantime.
Interactive Server
Everything works, because JavaScript interop works over the circuit. What changes is where the bytes travel: the browser decodes an image and the pixels are streamed to the server for processing, then the result is streamed back to be encoded. For a 4000 × 3000 photo that is 48 MB in each direction - unremarkable on a local network, not over a poor mobile connection. If images are central to an Interactive Server app:
- Constrain the working size aggressively:
options.MaxInteractiveDimension = 1200. - Use
DecodeOptions.MaxSizeso the browser downscales before the pixels are sent at all. - Prefer the headless API on the server for bulk work, and use the browser only for what needs a browser.
- Consider whether the image-heavy pages belong in the client project, with WebAssembly.
IImageCapabilityProvider reports ImageEnvironment.BrowserServer there,
so code can adapt.
Standalone WebAssembly
One project, no prerendering: register the services in Program.cs and link the
stylesheet from wwwroot/index.html.
// Program.csusing Bit.Bmage;using Microsoft.AspNetCore.Components.Web;using Microsoft.AspNetCore.Components.WebAssembly.Hosting;var builder = WebAssemblyHostBuilder.CreateDefault(args);builder.RootComponents.Add<App>("#app");builder.RootComponents.Add<HeadOutlet>("head::after");builder.Services.AddBitBmageServices();await builder.Build().RunAsync();<!-- wwwroot/index.html --><head> ... <link rel="stylesheet" href="_content/Bit.Bmage.Editor/bit-bmage-editor.css" /></head>Compile ahead of time
All image processing is C#, and on Blazor's default IL interpreter it runs 10 to 35 times slower than compiled code. Compiling the browser project ahead of time brings it to within 1.2 to 2.4 times of desktop .NET - see Performance for the measurements.
<PropertyGroup> <RunAOTCompilation>true</RunAOTCompilation></PropertyGroup>- It needs the
wasm-toolsworkload:dotnet workload install wasm-tools. - It only applies to
dotnet publish.dotnet runand the debugger always use the interpreter, so judge performance on a published build. - The download grows (the runtime's
dotnet.native.wasmwent from 2.9 MB to 13 MB uncompressed) and publishing takes minutes rather than seconds.
This site switches it on for a Release publish only - so its deployment is compiled ahead of
time while a plain dotnet build -c Release still works on a machine without the
workload. In a Web App you publish the server project and the client is compiled as part of it;
either pass -p:RunAOTCompilation=true to that publish, or condition it in the
client project the way this site does.
Without a browser
On a server, in a console app or a background worker, register the headless services instead.
There is no JavaScript and no probing, so ImageProcessor is registered directly.
Only the managed codecs exist: PNG, JPEG, BMP and GIF for decoding; PNG, JPEG and BMP for
encoding.
using Bit.Bmage;builder.Services.AddBitBmageHeadlessServices(options =>{ options.Limits = ImageLimits.Default with { MaxPixels = 30_000_000 };});// Then inject ImageProcessor wherever you need it.app.MapPost("/thumbnail", async (IFormFile file, ImageProcessor processor) => { /* ... */ });The Headless API page runs the same engine in your browser with no components, and shows a complete upload endpoint.