loading
Note:
Not a drawing API, on purpose Every path, gradient and text call from C# would be an interop round trip, and a drawing loop written that way is slower than the same loop in JavaScript by orders of magnitude. If you are drawing, write the drawing in a script. What C# genuinely could not do before is the other half - taking a frame and turning it into bytes - and that is what this covers.

Support check

IsSupported / IsOffscreenSupported / IsWebGlSupported / IsWebGpuSupported

A 2D context is everywhere. OffscreenCanvas is what Capture uses when it is there, and Capture works either way. WebGL and WebGPU are probes rather than wrappers - Butil does not wrap either, but 'can this device do it at all' is often the question behind the feature.

C#
@inject Bit.Butil.Canvas canvas

var supported = await canvas.IsSupported();
var offscreen = await canvas.IsOffscreenSupported();
var webgl = await canvas.IsWebGlSupported();
var webgpu = await canvas.IsWebGpuSupported();
Live sample
support check output
Results will appear here when you interact with the samples.

Draw an image onto a canvas

DrawImage / SetSize / Clear / GetSize

Source coordinates are in the source's own intrinsic pixels, not the size CSS is displaying it at - so a video laid out at 320px still yields its full frame. Note what GetSize reports: the pixel buffer and the CSS box are two different sizes, and the two disagreeing is the usual cause of a blurry canvas. Setting either dimension clears the canvas, even to the value it already had.

C#
private ElementReference _canvas;
private ElementReference _image;

await canvas.SetSize(_canvas, 320, 160);
await canvas.DrawImage(_canvas, _image);

// or just a corner of it, in a corner of the canvas:
await canvas.DrawImage(_canvas, _image, new CanvasDrawOptions
{
    SourceWidth = 100, SourceHeight = 50,
    DestinationWidth = 160, DestinationHeight = 80
});
Live sample
Source
sample
Canvas
draw output
Results will appear here when you interact with the samples.

Export what's on it

ToDataUrl / ToBytes

A data URL is base64, so it is about a third larger than the bytes it carries and it all sits in a string - fine for a preview, wasteful for anything else. ToBytes is what you upload or hash. An unsupported MIME type silently falls back to PNG rather than failing, which is worth knowing when a WebP export comes back looking like a PNG.

C#
var url = await canvas.ToDataUrl(_canvas);                       // <img src="...">
var png = await canvas.ToBytes(_canvas);                         // upload this
var jpeg = await canvas.ToBytes(_canvas, "image/jpeg", 0.7);     // smaller, lossy
Live sample
This section's canvas
sample
export output
Results will appear here when you interact with the samples.

A thumbnail, with no canvas of your own

Capture / CaptureToDataUrl

Capture draws into a scratch canvas it discards afterwards, so nothing has to exist in your markup. Give one dimension and leave the other at 0 to keep the aspect ratio - a thumbnail is one number, not two. The source's intrinsic size is what gets captured, not the size CSS is showing it at.

C#
// 120px wide, height follows the aspect ratio
var thumbnail = await canvas.Capture(_image, width: 120, type: "image/jpeg", quality: 0.7);

// or straight to something you can show
var url = await canvas.CaptureToDataUrl(_image, width: 120);
Live sample
Source sample
thumbnail output
Results will appear here when you interact with the samples.

A still from the camera

Capture from a video element

The headline case: MediaDevices opens the stream, a video element shows it, and Capture takes a frame as bytes - the whole path from camera to upload, in C#. Needs a secure context and the camera permission, so this one prompts.

C#
private ElementReference _video;

var stream = await mediaDevices.GetUserMedia(audio: false, video: true);
await stream!.AttachTo(_video);

// ... and later, a frame:
var photo = await canvas.Capture(_video, width: 640, type: "image/jpeg", quality: 0.85);
Live sample
camera output
Results will appear here when you interact with the samples.
Warning:
Tainted canvases cannot be read A canvas that has drawn a cross-origin image cannot be exported at all - the browser refuses, to stop a page using a canvas to read pictures the user can see but the page is not allowed to have. Every export here answers with null in that case rather than throwing, because drawing someone else's picture is a normal thing to do. Serve the image same-origin, or with CORS headers and crossorigin="anonymous" on the element.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when a 2D context can be created. Returns default (false) during prerender/SSR instead of throwing.
IsOffscreenSupported / IsWebGlSupported / IsWebGpuSupported
ValueTask<bool> ...()
Capability probes. Capture uses OffscreenCanvas where it exists and a detached element where it does not; WebGL and WebGPU are not wrapped.
GetSize
ValueTask<CanvasSize?> GetSize(ElementReference canvas)
The pixel buffer's size, the CSS box's size, and the device pixel ratio. Null when the element is not a canvas.
SetSize
ValueTask<bool> SetSize(ElementReference canvas, int width, int height)
Resizes the pixel buffer. Setting either dimension clears the canvas, even to the value it already had.
Clear
ValueTask<bool> Clear(ElementReference canvas)
Erases everything, leaving the canvas transparent.
DrawImage
ValueTask<bool> DrawImage(ElementReference canvas, ElementReference source, CanvasDrawOptions? options = null)
Draws a video, image or canvas onto a canvas. False when the source has nothing to draw yet - a video before its first frame, most often.
ToDataUrl
ValueTask<string?> ToDataUrl(ElementReference canvas, string type = "image/png", double quality = 0.92)
Encodes as a data: URL. Null when the canvas is tainted by a cross-origin image.
ToBytes
ValueTask<byte[]?> ToBytes(ElementReference canvas, string type = "image/png", double quality = 0.92)
Encodes as image bytes. Null when the canvas is tainted.
Capture
ValueTask<byte[]?> Capture(ElementReference source, int width = 0, int height = 0, string type = "image/png", double quality = 0.92)
Takes a picture from a video, image or canvas with no canvas of your own. One dimension at 0 keeps the aspect ratio; both at 0 uses the source's own size.
CaptureToDataUrl
ValueTask<string?> CaptureToDataUrl(ElementReference source, int width = 0, int height = 0, string type = "image/png", double quality = 0.92)
The same capture, encoded as a data: URL.
CanvasSize
record (int Width, int Height, double CssWidth, double CssHeight, double DevicePixelRatio)
The buffer size and the CSS size. Set the buffer to the CSS size times the ratio for a sharp canvas.
CanvasDrawOptions
class { double? SourceX, SourceY, SourceWidth, SourceHeight, DestinationX, DestinationY, DestinationWidth, DestinationHeight }
Which part of the source, and where on the canvas. All optional - the default stretches the whole source over the whole canvas.
An unhandled error has occurred. Reload 🗙