Skip to main content

Crate meo_skia_canvas

Crate meo_skia_canvas 

Source
Expand description

A multi-threaded, GPU-accelerated 2D graphics environment for Rust and Node: the HTML Canvas API on Skia, with GUI windows, animation, and export to raster and vector formats.

§Rust consumers: start at the crate root

Every public type is reachable as meo_skia_canvas::Thing, so nothing has to be looked up by module first. The modules group them by subject for reading; the prelude globs the same set for anyone who prefers one. No signature anywhere in the crate exposes a skia_safe or neon type, gui included, and the Node/Neon binding lives under the internal node module. scripts/check-public-api.mjs reads rustdoc’s JSON on every push and fails on a leak, with no module exempted – the claim is checked rather than maintained by hand.

§The shape of it

Canvas + Context2D are the API. Method names and argument order match CanvasRenderingContext2D, so knowledge carries over from JavaScript: a graphics state you mutate – fill style, transform, clip – and an encode straight to PNG, JPEG, WebP, GIF, APNG, TIFF, ICO, BMP, AVIF, PDF or SVG – or to raw pixels in the surface’s own layout, which is the twelfth of the formats counted above.

Everything else is the vocabulary those two speak – RgbaLinear, Path2D, Shader, Image, the filters, the text types – and one draw usually reaches across several, which is why they are at the root rather than behind their modules.

Drawing something and saving it:

use meo_skia_canvas::prelude::*;

let mut canvas = Canvas::new(800.0, 400.0);
{
    let ctx = canvas.context();
    ctx.set_fill_style(RgbaLinear::opaque(0.05, 0.05, 0.08));
    ctx.fill_rect(0.0, 0.0, 800.0, 400.0);

    ctx.set_fill_style(RgbaLinear::opaque(1.0, 0.35, 0.2));
    ctx.set_font(&Font::new("Helvetica", 48.0));
    ctx.fill_text("Hello", 40.0, 120.0, None);
}

canvas.to_file("hello.png", &EncodeOptions::default())?;

A canvas composites in the color space it was built with, and an export converts out of it:

use meo_skia_canvas::prelude::*;

let mut canvas = Canvas::with_options(
    1920.0,
    1080.0,
    CanvasOptions {
        color_space: PixelColorSpace::DisplayP3,
        ..CanvasOptions::default()
    },
)?;

let ctx = canvas.context();
// Wider than sRGB can name, and the canvas is built to hold it.
ctx.set_fill_style(RgbaLinear::opaque(1.0, 0.0, 0.0));
ctx.fill_rect(100.0, 100.0, 200.0, 100.0);

let pixels = ctx.get_image_data(0.0, 0.0, 200.0, 100.0)?;

§Colors are premultiplied and linear

RgbaLinear is not the 0-255 sRGB triple a CSS color parses to. Components are linear-light and premultiplied by alpha, so RgbaLinear::opaque(0.5, 0.5, 0.5) is not mid-gray on screen – it encodes to sRGB byte 188.

When porting a CSS color, use the sRGB constructors and the conversion happens for you:

use meo_skia_canvas::prelude::*;

let grey = RgbaLinear::from_srgb8(0x80, 0x80, 0x80, 1.0); // "#808080"
let same = RgbaLinear::from_hex("#808080")?;
let translucent = RgbaLinear::from_srgb8(255, 0, 0, 0.5); // rgba(255,0,0,.5)

Or skip the conversion: the fill and stroke styles take a CSS string directly, which is the shortest path when the color arrives as one. Unlike a browser, which ignores a color it cannot parse, these say so.

use meo_skia_canvas::prelude::*;

let mut canvas = Canvas::new(100.0, 100.0);
let ctx = canvas.context();

ctx.set_fill_style_css("#e33")?;
ctx.set_stroke_style_css("rgba(0, 0, 0, 0.4)")?;
assert!(ctx.set_fill_style_css("chartreuce").is_err()); // misspelled

See docs/rust.md in the repository for a longer reference (color spaces, alpha semantics, surfaces, paint, paths, shaders, filters, images, text, fonts).

§What runs on which thread

“Multi-threaded” in the line at the top means two pools, and they do different halves of an export.

Encoding runs on rayon. Writing a sequence hands every page to the pool at once, and writing an animation does it a batch at a time so frames reach the container in order – so RAYON_NUM_THREADS sizes the compressors, and on a machine with cores to spare that is where the time goes.

Rasterizing on the GPU does not. A Skia DirectContext belongs to the thread that made it, so letting each rayon worker have one meant as many contexts as workers, each cold and each holding its own resource cache; resident memory grew with the pool and an export paid to warm every context it touched. A bounded few threads own a context instead – four, or fewer on a smaller machine – and a worker submits its page, waits, and compresses the pixels it gets back where it already is. Nothing texture-backed crosses between them.

Two consequences worth knowing. Peak memory follows the number of owners rather than the size of the rayon pool, so raising RAYON_NUM_THREADS buys encoding throughput without buying contexts. And none of this makes a Canvas shareable: it is neither Send nor Sync, it stays on the thread that made it, and the threads above are the crate’s own – reached underneath a call that blocks until it has an answer.

That is a compile error rather than a convention, which is what lets the owners hold a Skia context safely. Sending one does not build:

use meo_skia_canvas::Canvas;
fn onto_a_thread<T: Send>(_: T) {}
onto_a_thread(Canvas::new(10.0, 10.0));

and neither does sharing one:

use meo_skia_canvas::Canvas;
fn between_threads<T: Sync>(_: &T) {}
between_threads(&Canvas::new(10.0, 10.0));

Rendering on the CPU has no owner and no context to belong to, so a page is rasterized wherever it is about to be encoded: both halves on the same worker, and nothing handed between them.

§What an export costs

PNG is the one format whose output depends on its own content. Both the row filtering and the deflate level are chosen by compressing a sample of the page two ways and keeping the cheaper, because what the deeper setting buys varies by more than the setting does – a page of flat interface colour gains nothing from it, and a dithered gradient gains most of its size. The answer is shared by the pages of one export and looked at again every sixteenth page, so a sequence that changes character partway is never far behind itself.

So two releases can write different bytes for the same drawing, and a PNG from this crate is not byte-comparable with one from another. The image is the same: PNG is lossless and both choices are reversible.

Nothing else adapts. JPEG, WebP and AVIF take the quality they are given, and PDF and SVG have no such choice to make.

§Rust callers are not batched

The Node binding records drawing calls into a buffer and hands them over in one crossing, because a call from JavaScript costs more crossing the boundary than the drawing behind it costs to do. There is no such boundary here. Context2D mutates the recording directly, so there is nothing to batch, nothing to flush, and no point at which a queued call has not happened yet.

§Cargo features

  • vulkan – enable the Vulkan backend (Linux / Windows).
  • metal – enable the Metal backend (macOS).
  • window – enable the winit-backed GUI window/event loop.
  • freetype – bundle FreeType + WOFF2 support for font registration on Linux containers / minimal images.
  • node-addon – register the #[neon::main] entry point so the resulting cdylib loads as a Node.js addon. Pure-Rust consumers should leave this off.

Pure-Rust consumers typically depend with default-features = false and pick the backend they need:

[dependencies]
meo-skia-canvas = { git = "https://github.com/l7aromeo/meo-skia-canvas", default-features = false, features = ["vulkan", "freetype"] }

Modules§

canvas
Entry point: the canvas, its pages, and the engine that draws them. The canvas document: pages in, encoded bytes out.
color
Colors and color spaces.
context2d
The stateful 2D drawing context, shaped like the Canvas API.
error
The crate’s error type.
export
Encoded output: image formats and the options that shape them.
filter
Image, color, and mask filters.
font
Font registration and variable-font axes.
geometry
Plain geometric value types shared across the public API.
gui
winit-backed windowing, behind the window feature.
image
Decoded raster images.
js_names
The seven types the two surfaces still spell differently.
memory
Returning freed memory to the operating system. Letting go of what a render left behind, once rendering stops.
paint
Fill and stroke styling.
path
Vector paths.
pattern
Tiled fill styles.
pixels
Pixel layouts for reading surfaces back and writing them.
prelude
Glob-importable re-export of the whole public API: use meo_skia_canvas::prelude::*;.
shader
Gradients and procedural shaders.
text
Text layout and styling.
texture
Hatch and stipple fill styles.

Structs§

Affine
2D affine transform in [a, b, c, d, tx, ty] form, matching the CSS DOMMatrix2DInit and CanvasRenderingContext2D.setTransform convention.
BackendInfo
What this build renders through, and what it found to render with.
Canvas
A canvas document, holding one page per Context2D.
CanvasOptions
What a canvas is built with, beyond its size.
ColorFilter
Color-domain filter (luma, gamma transfers, color matrix, compose).
ColorMatrix
A 4x5 color matrix, in the form ColorFilter::matrix takes.
Context2D
The drawing surface of a Canvas page.
DashPattern
An on/off dash pattern for stroked paths.
EncodeOptions
Settings applied while encoding.
Font
A font selection: families, size, weight, and slant.
FontAxisTag
Four-byte OpenType axis tag (e.g. "wght", "wdth", "opsz").
FontFamily
What a font family offers, as FontLibrary.family() reports it in JavaScript.
FontFeature
One OpenType feature applied to a text run, mirroring CanvasKit’s TextFontFeatures { name, value }.
FontLibrary
Owned font registry for the Rust facade.
FontVariation
Variable-font axis position.
GradientInterpolation
How a gradient interpolates between its stops: a colour space, and the direction hue travels within it.
GradientStop
One color stop in a gradient.
Image
An immutable decoded raster image.
ImageData
An owned pixel buffer read back from a canvas, together with the layout needed to interpret it.
ImageFilter
Image-domain filter (blur, drop shadow, color matrix wrapped as image filter, compose).
InvalidFontAxisTag
Returned by FontAxisTag’s FromStr impl when the input is not a 4-character ASCII string.
LineMetrics
Per-line layout metrics. start_index and end_index are byte offsets into the laid-out paragraph text.
MaskFilter
Coverage-mask filter applied before rasterization.
Paint
A paint’s blend mode, cap and join, as the drawing state carries them.
Paragraph
Result of TextEngine::layout_text.
ParagraphBuilder
A paragraph under construction, from TextEngine::paragraph_builder.
Path2D
Vector path.
PathBuilder
Builds a Path2D segment by segment.
Pattern
A repeating fill built from an image or another canvas.
PixelExportOptions
Layout to read a surface back in, or write one from.
Placeholder
A box reserved in a paragraph for something the text engine does not draw.
Point
A point in surface space.
Point3
A point in the space a lighting filter lights, with z toward the viewer.
Projection
A 3x3 transform, carrying the projective row an Affine cannot.
Rect
An axis-aligned rectangle, stored as its four edges.
RgbaLinear
A premultiplied color in linear light.
RichTextSpan
One span of rich text.
Shader
Public shader handle used by Paint::set_shader.
Size
A width/height pair, with no position.
StrutStyle
A fixed line box independent of the per-run fonts, for deterministic leading (captions, subtitles, vertically-aligned blocks).
TextBox
One rectangle covering part of a laid-out run, and the direction the text inside it reads.
TextDecoration
Underline / overline / line-through flags.
TextEngine
Builds laid-out text from a TextStyle and a maximum line width.
TextMetrics
Measurements of a text run, as measureText reports them.
TextMetricsLine
One line of a measured run, and the single-font stretches inside it.
TextMetricsRun
One stretch of a line drawn in a single font.
TextPosition
A position within laid-out text, as Paragraph::glyph_position_at_coordinate reports it.
TextShadow
Drop shadow applied behind glyphs. Multiple shadows on a single TextStyle stack additively.
TextStyle
Paragraph style.
Texture
A fill made of a repeated vector mark.
TextureOptions
How a texture’s repeating mark is drawn.

Enums§

Affinity
Which side of a character boundary a text position sits on.
BlendMode
Canvas-compatible blend modes, plus three CanvasKit ones.
BlurStyle
Coverage-mask blur style. Mirrors CanvasKit’s BlurStyle.
ChromaSampling
How many chroma samples an encoder writes per pixel.
ColorAxis
Which color axis ColorMatrix::rotated turns around.
ColorChannel
One channel of a colour, for the filters that read a single one.
DashFit
How a dash marker is placed along the path it follows.
EngineKind
The rasterizer a canvas ended up using.
Error
Everything this crate can fail with.
FillRule
Path2D winding rule.
FilterOp
One step of a CSS-style filter chain.
FontStretch
How wide a face to select within a family.
FontVariantCaps
A capitals variant, as CSS font-variant-caps.
GradientColorSpace
Color space a gradient’s stops are interpolated in.
HueMethod
Which way around the colour wheel a gradient’s hue travels.
ImageFormat
A container format for encoded output.
PaintSource
What a fill or a stroke draws with.
PaintStyle
Whether a draw fills its geometry or strokes its outline.
PathOp
A boolean operation between two paths.
PathSegment
One drawing command from a path, as Path2D::edges reports it.
PatternRepeat
Which axes a pattern repeats along.
PixelColorSpace
Strict export color space for surface read/write.
PixelDepth
Pixel layout a surface is read back in, or written from.
PixelFormat
Channel layout and alpha mode of a raw frame.
PlaceholderAlignment
Where an inline placeholder sits relative to the line it is on.
PlaceholderBaseline
Which baseline PlaceholderAlignment::Baseline aligns against.
RectHeightStyle
How tall the rectangles Paragraph::rects_for_range returns are.
RectWidthStyle
How wide the rectangles Paragraph::rects_for_range returns are.
SamplingMode
Image sampling strategy for draw_image_src and similar resampled draws.
SmoothingQuality
How much work the filter does when an image is drawn at a size other than its own.
StrokeCap
How the ends of an open stroked path are drawn.
StrokeJoin
How two stroked segments are joined where they meet.
TextAlign
Horizontal alignment of text within its layout width.
TextBaseline
Which horizontal line of the font a text draw sits on.
TextDecorationStyle
How a decoration line is drawn.
TextDirection
The reading direction a run is laid out in.
TextHeightBehavior
How the line-height multiplier is applied to the first ascent and last descent of a paragraph.
TextSlant
Whether glyphs are upright or slanted.
TileMode
What a filter does with the area outside its input.

Constants§

DEFAULT_HEIGHT
The height half of DEFAULT_WIDTH.
DEFAULT_WIDTH
The size a canvas has when nobody names one.