loading

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

ModelWhat worksWhat to know
Blazor WebAssemblyEverythingThe intended home. Processing runs on the user's device with no network involved.
Blazor Web App, InteractiveWebAssemblyEverythingSame as above once the component is interactive. This site.
Blazor Web App, InteractiveAutoEverything once interactiveThe first visit runs on the server while the runtime downloads - see Interactive Server.
Blazor Web App, InteractiveServerEverythingWorks, but pixels cross the circuit.
PrerenderingManaged codecs onlyNo browser exists yet. Call browser features from OnAfterRenderAsync.
ASP.NET Core, console, workerHeadless APIAddBitBmageHeadlessServices. 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.

C#
// 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;    }}
C#

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.

C#
// 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.

Razor
<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.

Razor
@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.MaxSize so 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.

C#
// 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();
HTML
<!-- 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.

XML
<PropertyGroup>  <RunAOTCompilation>true</RunAOTCompilation></PropertyGroup>
  • It needs the wasm-tools workload: dotnet workload install wasm-tools.
  • It only applies to dotnet publish. dotnet run and the debugger always use the interpreter, so judge performance on a published build.
  • The download grows (the runtime's dotnet.native.wasm went 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.

XML

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.

C#
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.

An unhandled error has occurred. Reload 🗙