Skia Canvas is a browser-less implementation of the HTML Canvas drawing API for Node.js. It is based on Google’s Skia graphics engine and, accordingly, produces very similar results to Chrome’s <canvas> element. The library is well suited for use on desktop machines where you can render hardware-accelerated graphics to a window and on the server where it can output a variety of image formats.
While the primary goal of this project is to provide a reliable emulation of the standard API according to the spec, it also extends it in a number of areas to take greater advantage of Skia's advanced graphical features and provide a more expressive coding environment.
In particular, Skia Canvas:
is fast and compact since rendering takes place on the GPU and all the heavy lifting is done by native code written in Rust and C++
can render to windows using an OS-native graphics pipeline and provides a browser-like UI event framework
generates images in both raster (JPEG, PNG, & WEBP) and vector (PDF & SVG) formats
If you’re running on a supported platform, installation should be as simple as:
npm install skia-canvas
This will download a pre-compiled library from the project’s most recent release.
Platform Support
The underlying Rust library uses N-API v8 which allows it to run on Node.js versions:
v12.22+
v14.17+
v15.12+
v16.0.0 and later
Pre-compiled binaries are available for:
Linux (x64 & arm64)
macOS (x64 & Apple silicon)
Windows (x64)
Nearly everything you need is statically linked into the library. A notable exception is the Fontconfig library which must be installed separately if you’re running on Linux.
Running in Docker
The library is compatible with Linux systems using glibc 2.28 or later as well as Alpine Linux (x64 & arm64) and the musl C library it favors. In both cases, Fontconfig must be installed on the system for skia-canvas to operate correctly.
If you are setting up a Dockerfile that uses node as its basis, the simplest approach is to set your FROM image to one of the (Debian-derived) defaults like node:lts, node:18, node:16, node:14-buster, node:12-buster, node:bullseye, node:buster, or simply:
FROM node
You can also use the ‘slim’ image if you manually install fontconfig:
FROM node:slim
RUN apt-get update && apt-get install -y -q --no-install-recommends libfontconfig1
If you wish to use Alpine as the underlying distribution, you can start with something along the lines of:
FROM node:alpine
RUN apk update && apk add fontconfig
Compiling from Source
If prebuilt binaries aren’t available for your system you’ll need to compile the portions of this library that directly interface with Skia.
Detailed instructions for setting up these dependencies on different operating systems can be found in the ‘Building’ section of the Rust Skia documentation. Once all the necessary compilers and libraries are present, running npm run build will give you a usable library (after a fairly lengthy compilation process).
Multithreading
When rendering canvases in the background (e.g., by using the asynchronous saveAs or toBuffer methods), tasks are spawned in a thread pool managed by the rayon library. By default it will create up to as many threads as your CPU has cores. You can see this default value by inspecting any Canvas object's engine.threads property. If you wish to override this default, you can set the SKIA_CANVAS_THREADS environment variable to your preferred value.
For example, you can limit your asynchronous processing to two simultaneous tasks by running your script with:
SKIA_CANVAS_THREADS=2 node my-canvas-script.js
Example Usage
Generating image files
import {Canvas} from'skia-canvas'let canvas = newCanvas(400, 400),
ctx = canvas.getContext("2d"),
{width, height} = canvas;
let sweep = ctx.createConicGradient(Math.PI * 1.2, width/2, height/2)
sweep.addColorStop(0, "red")
sweep.addColorStop(0.25, "orange")
sweep.addColorStop(0.5, "yellow")
sweep.addColorStop(0.75, "green")
sweep.addColorStop(1, "red")
ctx.strokeStyle = sweep
ctx.lineWidth = 100
ctx.strokeRect(100,100, 200,200)
// render to multiple destinations using a background threadasyncfunctionrender(){
// save a ‘retina’ image...await canvas.saveAs("rainbox.png", {density:2})
// ...or use a shorthand for canvas.toBuffer("png")let pngData = await canvas.png// ...or embed it in a stringlet pngEmbed = `<img src="${await canvas.toDataURL("png")}">`
}
render()
// ...or save the file synchronously from the main thread
canvas.saveAsSync("rainbox.pdf")
Multi-page sequences
import {Canvas} from'skia-canvas'let canvas = newCanvas(400, 400),
ctx = canvas.getContext("2d"),
{width, height} = canvas
for (const color of ['orange', 'yellow', 'green', 'skyblue', 'purple']){
ctx = canvas.newPage()
ctx.fillStyle = color
ctx.fillRect(0,0, width, height)
ctx.fillStyle = 'white'
ctx.arc(width/2, height/2, 40, 0, 2 * Math.PI)
ctx.fill()
}
asyncfunctionrender(){
// save to a multi-page PDF fileawait canvas.saveAs("all-pages.pdf")
// save to files named `page-01.png`, `page-02.png`, etc.await canvas.saveAs("page-{2}.png")
}
render()
This project is deeply indebted to the work of the Rust Skia project whose Skia bindings provide a safe and idiomatic interface to the mess of C++ that lies underneath. Many thanks to the developers of node-canvas for their terrific set of unit tests. In the absence of an Acid Test for canvas, these routines were invaluable.
Notable contributors
@mpaparno contributed support for SVG rendering, raw image-buffer handling, WEBP import/export and numerous bugfixes
@Salmondx developed the initial Raw image loading & rendering routines
@lucasmerlin helped get GPU rendering working on Vulkan
Documentation is now hosted at skia-canvas.org. Go there for a more readable version of all the details that used to be wedged into the README file.
Imagery
Added initial SVG rendering support. Images can now load SVG files and can be drawn in a resolution-independent manner via [drawImage()][mdn_drawImage] (thanks to @mpaperno #180). Note that Images loaded from SVG files that don't have a width and height set on their root <svg> element have some quirks as of this release:
The Image object's height will report being 150 and the width will be set to accurately capture the image's aspect ratio
When passed to drawImage() without size arguments, the SVG will be scaled to a size that fits within the Canvas's current bounds (using an approach akin to CSS's object-fit: contain).
When using the 9-argument version of drawImage(), the ‘crop’ arguments (sx, sy, sWidth, & sHeight) will correspond to this scaled-to-fit size, not the Image's reported width & height.
WEBP support
Canvas.[saveAs()][Canvas.saveAs] & [toBuffer()][Canvas.toBuffer] can now generate WEBP images and Images can load WEBP files as well (contributed by @mpaperno #177, h/t @revam for the initial work on this)
Raw pixel data support
The toBuffer() and saveAs() methods now support "raw" as a format name and/or file extension, causing them to return non-encoded pixel data (by default in an "rgba" layout like a standard [ImageData][ImageData] buffer)
Both functions now take an optional [colorType][colorType] argument to specify alternative pixel data layouts (e.g., "rgb" or "bgra")
[ImageData][ImageData] enhancements
The [drawImage()][mdn_drawImage] and [createPattern()][mdn_createPattern] methods have been extended to accept ImageData objects as arguments. Previously only [putImageData()][mdn_putImageData] could be used for rendering, but this method ignores the context's current transform, filters, opacity, etc.
When creating an ImageData via the [getImageData()][mdn_getImageData] & [createImageData()][mdn_createImageData] methods or new ImageData() constructor, the optional settings arg now allows you to select the colorType for the buffer's pixels.
Typography
FontLibrary.[use()][FontLibrary.use] now supports dynamically loaded [WOFF & WOFF2][woff_wiki] fonts
The [outlineText()][outline_text] method now takes an optional width argument and supports all the context's typographic settings (e.g., .font, .fontVariant, .textWrap, .textTracking, etc.)
Fonts with condensed/expanded widths can now be selected with the [.fontStretch][fontStretch] property. Note that stretch values included in the .font string will overwrite the current .fontStretch setting (or will reset it to normal if omitted).
Generic font family names are now mapped to fonts installed on the system. The serif, sans-serif, monospace, and system-ui families are currently supported.
Underlines, overlines, and strike-throughs can now be set via the Context's .textDecoration property.
Text spacing can now be fine-tuned using the [.letterSpacing][letterSpacing] and [.wordSpacing][wordSpacing] properties.
GUI
The [Window][window] class now has a [resizable][resizable] property which can be set to false to prevent the window from being manually resized or maximized (contributed by @nornagon #124).
Window [event handlers][win_bind] now support Input Method Editor events for entering composed characters via the [compositionstart][compositionstart], [compositionupdate][compositionupdate], & [compositionend][compositionend] events. The [input][input] event now reports the composed character, not the individual keystrokes.
Rendering
The Canvas object has a new engine property which describes whether the CPU or GPU is being used, which graphics device was selected, and what (if any) error prevented it from being initialized.
The .transform and .setTransform methods on Context, Path2D, and CanvasPattern objects can now take their arguments in additional formats. They can now be passed a [DOMMatrix][DOMMatrix] object or a string with a list of transformation operations compatible with the [CSS transform][css_transform] property. The DOMMatrix constructor also supports these strings as well as plain, matrix-like objects with numeric attributes named a, b, c, d, e, & f (contributed by @mpaperno #178).
The number of background threads used for asynchronous exports can now be controlled with the [SKIA_CANVAS_THREADS][multithreading] environment variable
Breaking Changes
An upgrade to [Neon][neon_rs] with [N-API v8][node_napi] raised the minimum required Node version to 12.22+, 14.17+, or 16+.
Images now load asynchronously in cases where the src property has been set to a local path. As a result, it's now necessary to await img.decode() or set up an .on("load", …) handler before drawing it—even when the src is non-remote.
The KeyboardEvent object returned by the keyup/keydown and input event listeners now has fields and values consistent with browser behavior. In particular, code is now a name (e.g., ShiftLeft or KeyS) rather than a numeric scancode, key is a straightforward label for the key (e.g., Shift or s) and the new [location][key_location] field provides a numeric description of which variant of a key was pressed.
The deprecated .async property has been removed. See the v0.9.28 release notes for details.
The non-standard .textTracking property has been removed in favor of the new [.letterSpacing][letterSpacing] property
Bugfixes
Initializing a GPU-renderer using Vulkan now uses the vulkano crate and makes better selections among devices present (previously it was just using the first result, which is not always optimal).
The Image.onload callback now properly sets this to point to the new image (contributed by @mpaperno & @ForkKILLET).
Creating a Window with fullscreen set to true now takes effect immediately (previously it was failing silently)
Drawing paths after setting an invalid transform no longer crashes (contributed by @mpaperno #175)
Windows with .on("draw") handlers no longer become unresponsive on macOS 14+ after being fully occluded by other windows
Ellipses with certain combinations of positive and negative start- and stop-angles now render correctly—previously they would not appear at all if the total sweep exceeded 360° (contributed by @mpaperno #176)
The drawCanvas() method now clips to the specified crop size (contributed by @mpaperno #179)
Hit-testing with [isPointInPath][isPointInPath()] and [isPointInStroke][isPointInStroke()] now works correctly when called with a Path2D object as the first argument
Added TypeScript definitions for the Window object’s event types (contributed by @saantonandre #163) and the roundRect method (contributed by @sandy85625 & @santilema)
Performance improvements to FontLibrary, speeding up operations like listing families and adding new typefaces.
Updated winit and replaced the end-of-life’d skulpin-based Vulkan renderer with a new implementation using Vulkano for window-drawing on Windows and Linux.
It’s a fairly direct adaptation of Vulkano [sample code][vulkano_demo] for device setup with skia-specific rendering routines inspired by @pragmatrix’s renderer for [emergent][pragmatrix_emergent]. All of which is to say, if you understand this better than I do I'd love some suggestions for improving the rendering setup.
The GPU is now initialized only when it is needed, not at startup. As a result, setting that Canvas's [.gpu][canvas_gpu] property to false immediately after creation will prevent any GPU-related resource acquisition from occurring (though rendering speed will be predictably slower).
The sample-count used by the GPU for multiscale antialiasing can now be configured through the optional [msaa][msaa] export argument. If omitted, defaults to 4x MSAA.
Added support for non-default imports (e.g., import {Image} from "skia-canvas") when used as an ES Module.
The [getImageData()][mdn_getImageData] method now makes use of the GPU (if enabled) and caches data between calls, greatly improving performance for sequential queries
The npm package skia-canvas receives a total of 11,956 weekly downloads. As such, skia-canvas popularity was classified as popular.
We found that skia-canvas demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.It has 0 open source maintainers collaborating on the project.
Package last updated on 02 Dec 2024
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Newly introduced telemetry in devenv 1.4 sparked a backlash over privacy concerns, leading to the removal of its AI-powered feature after strong community pushback.
TC39 met in Seattle and advanced 9 JavaScript proposals, including three to Stage 4, introducing new features and enhancements for a future ECMAScript release.