Skip to content

samizdatco/skia-canvas

Repository files navigation

Skia Canvas
Getting Started   ·   Documentation   ·   Release Notes   ·   Discussion Forum

Skia Canvas is a Node.js implementation of the HTML Canvas drawing API for both on- and off-screen rendering. Since it uses Google’s Skia graphics engine, its output is very similar to Chrome’s <canvas> element — though it's also capable of things the browser’s Canvas still can't achieve.

In particular, Skia Canvas:

Installation

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.

pnpm

If you use the pnpm package manager, it will not download skia-canvas's platform-native binary unless you explicitly allow it. You can do this interactively via the ‘approve builds’ command (note that you need to press <space> to toggle the selection and then <enter> to proceed):

pnpm install skia-canvas
pnpm approve-builds

In non-interactive scenarios (like building via CI), you can approve the build step when you add skia-canvas to your project:

pnpm install skia-canvas --allow-build=skia-canvas

Alternatively, you can add a pnpm.onlyBuiltDependencies entry to your package.json file to mark the build-step as allowed:

{
  "pnpm": {
    "onlyBuiltDependencies": ["skia-canvas"]
  }
}

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 & arm64
  • AWS Lambda — x64 & arm64

Linux / Docker

The library is compatible with Linux systems using glibc 2.28 or later as well as Alpine Linux and the musl C library it favors. In both cases, the Fontconfig library 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

AWS Lambda

Skia Canvas depends on libraries that aren't present in the standard Lambda runtime. You can add these to your function by uploading a ‘layer’ (a zip file containing the required libraries and node_modules directory) and configuring your function to use it.

  1. Look in the Assets section of Skia Canvas’s current release and download the aws-lambda-x64.zip or aws-lambda-arm64.zip file (depending on your architecture) but don’t decompress it
  2. Go to the AWS Lambda Layers console and click the Create Layer button, then fill in the fields:
  • Name: skia-canvas (or whatever you want)
  • Description: you might want to note the Skia Canvas version here
  • Compatible architectures: select x86_64 or arm64 depending on which zip you chose
  • Compatible runtimes: select Node.js 22.x (and/or 20.x & 18.x)
  1. Click the Choose file button and select the zip file you downloaded in Step 1, then click Create

You can now use this layer in any function you create in the Functions console. After creating a new function, click the Add a Layer button and you can select your newly created Skia Canvas layer from the Custom Layers layer source.

Next.js / Webpack

If you are using a framework like Next.js that bundles your server-side code with Webpack, you'll need to mark skia-canvas as an ‘external’, otherwise its platform-native binary file will be excluded from the final build. Try adding these options to your next.config.ts file:

const nextConfig: NextConfig = {
  serverExternalPackages: ['skia-canvas'],
  webpack: (config, options) => {
    if (options.isServer){
      config.externals = [
        ...config.externals,
        {'skia-canvas': 'commonjs skia-canvas'},
      ]
    }
    return config
  }
};

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.

Start by installing:

  1. The Rust compiler and cargo package manager using rustup
  2. A C compiler toolchain (either LLVM/Clang or MSVC)
  3. Python 3 (used by Skia's build process)
  4. The Ninja build system
  5. On Linux: Fontconfig and OpenSSL

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

Global Settings

There are a handful of settings that can only be configured at launch and will apply to all the canvases you create in your script. The sections below describe the different environment variables you can set to make global changes. You can either set them as part of your command line invocation, or place them in a .env file in your project directory and use Node 20's --env-file argument to load them all at once.

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

Argument Validation

There are a number of situations where the browser API will react to invalid arguments by silently ignoring the method call rather than throwing an error. For example, these lines will simply have no effect:

ctx.fillRect(0, 0, 100, "october")
ctx.lineTo(NaN, 0)

Skia Canvas does its best to emulate these quirks, but allows you to opt into a stricter mode in which it will throw TypeErrors in these situations (which can be useful for debugging).

Set the SKIA_CANVAS_STRICT environment variable to 1 or true to enable this mode.

Example Usage

Generating image files

import {Canvas} from 'skia-canvas'

let canvas = new Canvas(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 thread
async function render(){
  // 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 string
  let 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 = new Canvas(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()
}

async function render(){
  // save to a multi-page PDF file
  await canvas.saveAs("all-pages.pdf")

  // save to files named `page-01.png`, `page-02.png`, etc.
  await canvas.saveAs("page-{2}.png")
}
render()

Rendering to a window

import {Window} from 'skia-canvas'

let win = new Window(300, 300)
win.title = "Canvas Window"
win.on("draw", e => {
  let ctx = e.target.canvas.getContext("2d")
  ctx.lineWidth = 25 + 25 * Math.cos(e.frame / 10)
  ctx.beginPath()
  ctx.arc(150, 150, 50, 0, 2 * Math.PI)
  ctx.stroke()

  ctx.beginPath()
  ctx.arc(150, 150, 10, 0, 2 * Math.PI)
  ctx.stroke()
  ctx.fill()
})

Integrating with Sharp.js

import sharp from 'sharp'
import {Canvas, loadImage} from 'skia-canvas'

let canvas = new Canvas(400, 400),
    ctx = canvas.getContext("2d"),
    {width, height} = canvas,
    [x, y] = [width/2, height/2]

ctx.fillStyle = 'red'
ctx.fillRect(0, 0, x, y)
ctx.fillStyle = 'orange'
ctx.fillRect(x, y, x, y)

// Render the canvas to a Sharp object on a background thread then desaturate
await canvas.toSharp().modulate({saturation:.25}).jpeg().toFile("faded.jpg")

// Convert an ImageData to a Sharp object and save a grayscale version
let imgData = ctx.getImageData(0, 0, width, height, {matte:'white', density:2})
await imgData.toSharp().grayscale().png().toFile("black-and-white.png")

// Create an image using Sharp then draw it to the canvas as an Image object
let sharpImage = sharp({create:{ width:x, height:y, channels:4, background:"skyblue" }})
let canvasImage = await loadImage(sharpImage)
ctx.drawImage(canvasImage, x, 0)
await canvas.saveAs('mosaic.png')

Acknowledgements

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 bug fixes
  • @Salmondx developed the initial Raw image loading & rendering routines
  • @lucasmerlin helped get GPU rendering working on Vulkan
  • @cprecioso & @saantonandre corrected and expanded upon the TypeScript type definitions
  • @meihuanyu contributed filter & path rendering fixes

Copyright

© 2020–2025 Samizdat Drafting Co.

About

A multi-threaded, GPU-powered, 2D vector graphics environment for Node.js

Topics

Resources

License

Stars

Watchers

Forks

Packages