Formats
PNG, JPEG, BMP and GIF are decoded by C# that runs anywhere; WebP and AVIF come from the browser's own codecs. Support varies by browser, version and platform, so Bmage probes instead of assuming - and refuses to pass off a substitute as the format you asked for.
Encode one image every way
The same 1200 × 800 scene, encoded at quality 0.82 in each format. A format this browser cannot write is attempted anyway, to show what the exporter does about it.
What decodes and encodes where
| Format | Managed decode | Managed encode | Browser decode | Browser encode |
|---|---|---|---|---|
| PNG | yes | yes | yes | yes |
| JPEG | yes | yes | yes | yes |
| BMP | yes | yes | yes | via managed |
| GIF | first frame | no | first frame | no |
| WebP | no | no | yes | usually |
| AVIF | no | no | usually | rarely |
| SVG | refused | no | refused | no |
"Managed" means pure C#, working anywhere including a server. "Browser" means the platform's own codecs, the only way to reach WebP and AVIF in a browser. When both are available the browser codec wins, because it is hardware accelerated and covers more formats; the managed codecs remain the fallback, which is what makes prerendering and server-side use work. The Capabilities page shows the probe's answer for this browser.
Never assume, always probe
Asked to encode a format they cannot write, browsers silently produce a PNG instead. Bmage probes by actually encoding a pixel and checking what came back, and detects the substitution at export time too.
var capabilities = await capabilityProvider.GetAsync();if (capabilities.CanEncode(ImageFormat.Avif)) { /* ... */ }if (capabilities.SupportsWebP) { /* ... */ }try{ var avif = await exporter.EncodeAsync(image, ImageExportOptions.Avif);}catch (ImageCapabilityException ex){ // "This browser cannot encode Avif; it produced image/png instead. Check // ImageCapabilities.CanEncode before exporting and fall back to a supported format."}If you would rather degrade than handle an exception, ask for a preference order:
var encoded = await exporter.EncodeWithFallbackAsync( image, [ImageFormat.Avif, ImageFormat.WebP, ImageFormat.Jpeg], new ImageExportOptions { Quality = 0.82 });Console.WriteLine(encoded.Format); // whichever one this browser can actually writeChoosing a format
| Situation | Choose |
|---|---|
| Photographs, broad compatibility | JPEG at 0.8 to 0.85 |
| Photographs, modern browsers | WebP at 0.8, roughly 25 to 35% smaller than JPEG at similar quality |
| Anything needing transparency | WebP, or PNG for lossless |
| Screenshots, diagrams, flat colour | PNG |
| Smallest possible, where supported | AVIF, but always with a fallback |
| Clipboard | PNG; most browsers accept nothing else |
Circular avatars need transparency, so prefer WebP or PNG there - a lossy opaque format shows the corners. The Avatar page switches its fallback for exactly that reason.
Quality and size budgets
var encoded = await exporter.EncodeAsync(image, new ImageExportOptions{ Format = ImageFormat.WebP, Quality = 0.9, MaxFileSize = 100 * 1024, MinQuality = 0.5, MaxFileSizeStrategy = MaxFileSizeStrategy.QualityThenDimensions,});Console.WriteLine($"{encoded.Length} bytes at quality {encoded.Quality}");MaxFileSize bisects on quality first, down to MinQuality, then shrinks
the dimensions if the strategy allows it. The quality actually used is reported on the result,
so you can show it. When the target is impossible you get an ImageEncodeException
explaining what the smallest attempt produced, rather than a silently oversized file.
| Strategy | Behaviour |
|---|---|
QualityThenDimensions | Lower quality first, then shrink. The default. |
QualityOnly | Never change dimensions; throw if the budget cannot be met. |
DimensionsOnly | Keep quality fixed and shrink. |
Transparency without an alpha channel
JPEG has no alpha channel. Exporting a transparent image as JPEG composites it over
ImageExportOptions.Background, which defaults to white - without it, browsers
composite over black, which is rarely what anyone wants. Set it explicitly when white is wrong
for your page.
new ImageExportOptions { Format = ImageFormat.Jpeg, Background = new Rgba32(18, 18, 20) }Format detection
Content is trusted over file names and content types, because both can be wrong or hostile.
ImageProcessor.Identify reads dimensions from the header without decoding any
pixels, which is how an oversized upload is rejected before anything is allocated.
DisplaySize accounts for EXIF orientation, so a portrait photo from a phone
reports portrait dimensions even though its stored pixels are landscape.
var format = ImageFormats.Detect(bytes); // magic bytesvar byName = ImageFormats.FromFileName("photo.JPG"); // convenience onlyvar byMime = ImageFormats.FromMimeType("image/webp"); // convenience onlyvar info = processor.Identify(bytes);if (info is null) return Error("Not a supported image.");if ((long)info.Width * info.Height > 40_000_000) return Error("Too large.");Console.WriteLine($"{info.Width}×{info.Height} {info.Format}, displays as {info.DisplaySize}");GIF and SVG
GIF decodes its first frame, with palettes, interlacing and transparency; animation is out of scope, and GIF encoding is supported by no browser canvas and is not implemented - use WebP or PNG.
SVG is refused with an explanatory error rather than rasterised. Rendering untrusted SVG can execute embedded scripts and fetch remote resources, and a library cannot make that safe on the caller's behalf. If you control the SVG, rasterise it yourself and pass the pixels.