Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

skia-canvas

samizdatco207.3kMIT3.0.2TypeScript support: included

A multi-threaded, GPU-accelerated, Canvas API for Node

canvas, gpu, skia, offscreen, headless, graphic, graphics, image, images, compositing, render, vulkan, metal, pdf, svg, rust

readme

<picture> <source media="(prefers-color-scheme: dark)" srcset="docs/assets/hero-dark@2x.png"> Skia Canvas </picture>
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

Skia Canvas runs on Linux, macOS, or Windows as well as serverless platforms like Vercel and AWS Lambda. Precompiled versions of the library’s native code will be automatically downloaded in the appropriate architecture (arm64 or x64) when you install it via npm.

The underlying Rust library uses N-API v8 which allows it to run on all currently supported Node.js releases, and it is backward compatible with versions going back to v12.22+, v14.17+, v15.12+, and v16+.

Linux

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. It will make use of the system’s fontconfig settings in /etc/fonts if they exist but will otherwise fall back to using a placeholder configuration, looking for installed fonts at commonly used Linux paths.

Docker

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:22, node:24-bookworm, or simply:

FROM node

If you wish to use Alpine as the underlying distribution, you can start with something along the lines of:

FROM node:alpine

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.

<summary>

Detailed AWS instructions

</summary>

Adding the Skia Canvas layer to your AWS account

  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)
  3. Click the Choose file button and select the zip file you downloaded in Step 1, then click Create

Alternatively, you can use the aws command line tool to create the layer. This bash script will fetch the skia-canvas version of your choice and make it available to your Lambda functions.

#!/usr/bin/env bash
VERSION=3.0 # the skia-canvas version to include
PLATFORM=arm64 # arm64 or x64

curl -sLO https://github.com/samizdatco/skia-canvas/releases/download/v${VERSION}/aws-lambda-${PLATFORM}.zip
aws lambda publish-layer-version \
    --layer-name "skia-canvas" \
    --description "Skia Canvas ${VERSION} layer" \
    --zip-file "fileb://aws-lambda-${PLATFORM}.zip" \
    --compatible-runtimes "nodejs20.x" "nodejs22.x" \
    --compatible-architectures "${X/#x/x86_}"

Using the layer in a Lambda function

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.

Note that the layer only includes Skia Canvas and its dependencies—any other npm modules you want to use will need to be bundled into your function. To prevent the skia-canvas module from being doubly-included, make sure you add it to the devDependencies section (not the regular dependencies section) of your package.json file.

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. A recent version of git (older versions have difficulties with Skia's submodules)
  2. The Rust compiler and cargo package manager using rustup
  3. A C compiler toolchain (either LLVM/Clang or MSVC)
  4. Python 3 (used by Skia's build process)
  5. The Ninja build system
  6. 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. The Dockerfiles in the containers directory may also be useful for identifying needed dependencies. 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 toFile 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')

Benchmarks

In these benchmarks, Skia Canvas is tested running in two modes: serial and async. When running serially, each rendering operation is awaited before continuing to the next test iteration. When running asynchronously, all the test iterations are begun at once and are executed in parallel using the library’s multi-threading support.

See full results here…

Startup latency

Library Per Run Total Time (100 iterations)
canvaskit-wasm    25 ms 2.46 s
canvas    88 ms 8.76 s
@napi-rs/canvas    73 ms 7.30 s
skia-canvas    <1 ms 33 ms

Bezier curves

Library Per Run Total Time (20 iterations)
canvaskit-wasm 👁️ 789 ms 15.77 s
canvas 👁️ 488 ms 9.76 s
@napi-rs/canvas 👁️ 233 ms 4.65 s
skia-canvas (serial) 👁️ 137 ms 2.74 s
skia-canvas (async) 👁️ 28 ms 558 ms

SVG to PNG

Library Per Run Total Time (100 iterations)
canvaskit-wasm ————— ————— not supported
canvas 👁️ 122 ms 12.20 s
@napi-rs/canvas 👁️ 98 ms 9.76 s
skia-canvas (serial) 👁️ 59 ms 5.91 s
skia-canvas (async) 👁️ 11 ms 1.06 s

Scale/rotate images

Library Per Run Total Time (50 iterations)
canvaskit-wasm 👁️ 279 ms 13.95 s
canvas 👁️ 284 ms 14.21 s
@napi-rs/canvas 👁️ 116 ms 5.78 s
skia-canvas (serial) 👁️ 100 ms 5.01 s
skia-canvas (async) 👁️ 19 ms 937 ms

Basic text

Library Per Run Total Time (200 iterations)
canvaskit-wasm 👁️ 24 ms 4.74 s
canvas 👁️ 24 ms 4.86 s
@napi-rs/canvas 👁️ 19 ms 3.82 s
skia-canvas (serial) 👁️ 21 ms 4.24 s
skia-canvas (async) 👁️ 4 ms 781 ms

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

© 2020–2025 Samizdat Drafting Co.

changelog

Changelog

📦 ⟩ v3.0.2 ⟩ Aug 17, 2025

Misc. Improvements

  • Only use node-fetch on systems lacking a built-in fetch
  • Dropped fast-glob (reducing external dependency count to 11)

Breaking Changes

  • Glob-handling has been removed from FontLibrary.use(). If you want the old behavior, try using the fast-glob or glob modules to prepare the file-list you pass to the method.

📦 ⟩ v3.0.1 ⟩ Aug 16, 2025

Misc. Improvements

  • Updated node-fetch to v3 to fix deprecation warnings on recent node versions
  • Updated winit and other rust dependencies

📦 ⟩ v3.0.0 ⟩ Aug 15, 2025

New Features

GUI

  • The App global now has an eventLoop property which can be set to:
    • "native" (the default) in which case the Node event loop is suspended while the OS handles displaying GUI windows
    • "node" where the Node event loop maintains control (allowing setInterval and setTimeout to run) and handles GUI events manually every few milliseconds (though note some of the caveats associated with the Winit feature this uses).
  • Window objects now have a read-only closed property and emit a close event when they are closed. Closed windows can later be re-opened by calling the new open() method.
  • The new borderless attribute allows Window titlebars and borders to be hidden (thanks to @hydroperx #230)

Imagery

  • The loadImage() and loadImageData() helpers now use node-fetch to handle web requests and can accept a fetch options object as the final argument.
  • Image objects can now be created by passing a Buffer or dataURL-containing string as a constructor argument and will be immeditately drawable (no asynchronous loading required).
  • Added support for integrating the Sharp image processor into canvas workflows (if the sharp npm module has been installed):
    • The new Canvas.toSharp() & ImageData.toSharp() convenience methods convert their contents to a Sharp bitmap object
    • loadImage() & loadImageData() can now be called with a Sharp object as their sole argument
    • The src property on a new Image object can be set to a Sharp object and it will begin asynchronously loading
  • Added new options to createTexture() for setting the line cap style and selecting whether vector patterns should be clipped or outlined

Rendering

  • Significant speed-ups for deeply layered drawing in which the canvas isn't cleared or reset (potentially resulting in numerous vector objects being re-drawn despite being hidden by shapes drawn on top):
    • The bitmap generated by getImageData()/toBuffer()/toFile() is now cached. When called repeatedly, only newly added drawing commands will need to be rasterized (and will be layered atop the bitmap saved in the prior call).
    • Window contents are now cached between screen refreshes, improving performance during resizing and in cases where the canvas is drawn to in multiple passes and not cleared with every frame
    • Calling clearRect() or fillRect() with an area that covers the canvas now erases all the vector shapes below
  • The toFile(), toBuffer(), and toDataURL() methods now accept an optional downsample flag (for jpegs only), which enables 4:2:0 chroma-subsampling. By default, no subsampling (a.k.a. 4:4:4) will be performed
  • The getImageData() method now accepts additional rendering arguments (density, matte, and msaa) which behave the same as their equivalents in the toFile() method.

Typography

  • Text lightness can now be fine-tuned through a pair of optional arguments that can be passed to the Canvas or Window constructors:
    • textContrast — a number in the range 0.0–1.0 controlling the amount of additional weight to add (defaults to 0.0)
    • textGamma — a number in the range 0.0–4.0 controlling how glyph edges are blended with the background (defaults to 1.4)
  • The textAlign attribute can now be set to "justify"
  • measureText() has been rewritten to calculate metrics based not just on the font specified in font but also any fallback fonts that were used for character glyphs not present in the ‘main’ font. The line-by-line measurements now include a runs array with bounds and metrics for each single-font range of characters on the line.

Supported Platforms

  • Added precompiled binaries for Arm-based Windows systems
  • Now providing pre-built ‘layer’ archives for use with AWS Lambda (for Node v20 and above)
  • Linux builds now include a statically linked version of fontconfig, as a result:
    • libfontconfig packages no longer need to be installed on the host system using apt, apk, yum, dnf, etc.
    • it now runs on ‘serverless’ platforms like Vercel without modification (sadly Cloudflare doesn't support native modules at all though)

Breaking Changes

  • Renamed export functions and options to be more consistent with similar browser APIs and other Node modules:
    • saveAs() and saveAsSync() are now called toFile() and toFileSync()
    • toDataURL() now behaves the same as its browser equivalent: it is synchronous and its only configuration option is a numerical quality setting
    • toDataURLSync() has been removed
    • toURL() and toURLSync() produce data URLs and support the same enhanced export options as toBuffer
  • When exporting to an SVG, text is now converted to paths only if the outline option is set to true

Misc. Improvements

  • App.launch() now returns a Promise that resolves when the final window is closed, allowing you to schedule code to run before the process would otherwise exit (see also the new idle event which fires under the same circumstances).
  • input event objects now contain an inputType property to distinguish between insertion, deletion, and IME composition
  • Mouse events are no longer coalesced down to a single instance per frame (most relevant for mousemove events)
  • Mouse events now include a standard buttons attribute
  • DPI metadata is now included in webp files (reflecting the density option passed to toFile() or toBuffer())
  • Argument validation now emulates browser behavior much more closely—including converting what were previously TypeErrors in certain cases into silent failures. To reënable these errors, set the SKIA_CANVAS_STRICT environment variable to 1 or true.
  • Replaced node-pre-gyp with a custom installation script and glob with fast-glob, cutting the number of node_modules directories installed from 83 to 29.
  • loadImage(), loadImageData(), and Image.src can now accept URL objects (using http(s), file, or data protocols). Likewise, toFile() now accepts file: URLs (allowing relative paths to be constructed with import.meta.url)
  • The Canvas constructor's options argument can now contain a gpu property which can be set to false in order to use CPU-based rendering

Bugfixes

  • Setting a window's cursor property to "none" now hides the cursor
  • Spurious moved window events are no longer emitted during resizes
  • resize events now update the window object’s width & height properties in addition to providing the new size in the event object
  • roundRect() now reflects context's current transform state and accepts plain {x, y} objects for corner-radii in addition to Numbers and DOMPoints (thanks to @mpaperno #223)
  • Angles passed to createConicGradient() are no longer incorrectly offset by 90°
  • Calling lineTo on an empty Path2D no longer adds a line from the origin to the specified coordinates: it now acts as if it were a moveTo
  • measureText() now correctly calculates widths when letterSpacing has been set
  • startRange and endRange in TextMetrics.lines[] now correspond to character indices in the string passed to measureText(), not byte indices into the UTF-8 buffer backing it

📦 ⟩ v2.0.2 ⟩ Jan 27, 2025

New Features

  • Added fontHinting attribute (off by default to better match font weights in browser rendering). Setting it to true may result in crisper edges but adds some weight to the font.

Bugfixes

  • Text spacing
    • Setting letterSpacing no longer indents text at beginning of line
    • letterSpacing now properly handles negative values
  • Improved accuracy of measureText()
    • Now uses font metrics' default leading when the line-height is left unspecified in the ctx.font string (NB: this is likely to cause vertical shifts for non-alphabetic baselines)
    • Updated baseline offset calculations for middle & hanging to better match browsers
    • The actualBoundingBox* & lines[].x/y/width/height rectangles returned by measureText() are now just the glyph-occupied area, not the whole line-height of the textblock
    • Fixed the sign on actualBoundingBoxLeft (positive values now mean left of the origin)
    • lines[].baseline now corresponds to the selected ctx.textBaseline, previously it was always the alphabetic baseline
  • TypeScript definitions no longer include the entire DOM library (which had been pulling in tons of non-Canvas-related object types that this library doesn't emulate)

📦 ⟩ v2.0.1 ⟩ Dec 8, 2024

Misc. Improvements

  • Added support for Intel integrated GPUs that would previously throw an "instantiated but unable to render" error
    • Note: you may need to upgrade to the latest Mesa drivers (24.3.1 or later), especially for in-window rendering to work correctly on Linux
  • Fixed window initialization for Vulkan GPUs that default to a framebuffer color-format Skia doesn't support
  • Vulkan drivers that fall back to the Mesa LLVMpipe software renderer now work correctly
  • Optimized font library initialization to improve SVG parsing speed

📦 ⟩ v2.0.0 ⟩ Dec 2, 2024

New Features

Website

  • 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() (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() & 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 buffer)
    • Both functions now take an optional colorType argument to specify alternative pixel data layouts (e.g., "rgb" or "bgra")
  • ImageData enhancements
    • The drawImage() and createPattern() methods have been extended to accept ImageData objects as arguments. Previously only 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() & 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() now supports dynamically loaded WOFF & WOFF2 fonts
  • The outlineText() 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 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 and .wordSpacing properties.

GUI

  • The Window class now has a 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 now support Input Method Editor events for entering composed characters via the compositionstart, compositionupdate, & compositionend events. The 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 object or a string with a list of transformation operations compatible with the 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 environment variable

Breaking Changes

  • An upgrade to Neon with N-API v8 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 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 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 and isPointInStroke now works correctly when called with a Path2D object as the first argument

Misc. Improvements

  • Upgraded Skia to milestone 131
  • 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 for device setup with skia-specific rendering routines inspired by @pragmatrix’s renderer for 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 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 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() method now makes use of the GPU (if enabled) and caches data between calls, greatly improving performance for sequential queries

📦 ⟩ v1.0.2 ⟩ Aug 21, 2024

Maintenance

  • After getting a surprise bill from Amazon for the S3 bucket hosting the pre-compiled binaries, I've moved them to GitHub Releases instead. Aside from resolving some security warnings by upgrading dependencies, this version should be functionally identical to 1.0.1…

Breaking Changes

  • The 32-bit ARM-based linux builds are no longer provided pre-compiled; you'll now need to build from source.

📦 ⟩ v1.0.1 ⟩ Oct 15, 2022

Bugfixes

  • If an offscreen buffer can't be allocated using the Vulkan renderer, CPU rendering is used as a fallback
  • The drawCanvas() routine now works even when the destination canvas is later saved as an SVG (previously, the source canvas would be missing from the output). Caveat: this only works if the destination canvas is using the default source-over blend mode, has its globalAlpha set to 1, and is not using shadows or the effect property. If any of those defaults have been changed, the drawn canvas will not appear in the saved SVG. Bitmap and PDF exports do not have this restriction.

Misc. Improvements

  • Added a fullscreen event to the Window class to flag changes into and out of full-screen mode.

📦 ⟩ v1.0.0 ⟩ Aug 5, 2022

New Features

  • The new Window class can display a Canvas on screen, respond to mouse and keyboard input, and fluidly animate by calling user-defined event handlers.
  • Bitmap rendering now occurs on the GPU by default and can be configured using the Canvas's .gpu property. If the platform supports hardware-accelerated rendering (using Metal on macOS and Vulkan on Linux & Windows), the property will be true by default and can be set to false to use the software renderer.
  • Added support for recent Chrome features:
    • the reset() context method which erases the canvas, resets the transformation state, and clears the current path
    • the roundRect() method on contexts and Path2D objects which adds a rounded rectangle using 1–4 corner radii (provided as a single value or an array of numbers and/or DOMPoint objects)

Bugfixes

  • The FontLibrary.reset() method didn't actually remove previously installed fonts that had already been drawn with (and thus cached). It now clears those caches, which also means previously used fonts can now be replaced by calling .use() again with the same family name.
  • The .drawCanvas() routine now applies filter effects and shadows consistent with the current resolution and transformation state.

Misc. Improvements

  • The .filter property's "blur(…)" and "drop-shadow(…)" effects now match browser behavior much more closely and scale appropriately with the density export option.
  • Antialiasing is smoother, particularly when down-scaling images, thanks to the use of mipmaps rather than Skia's (apparently buggy?) implementation of bicubic interpolation.
  • Calling clearRect() with dimensions that fully enclose the canvas will now discard all the vector objects that have been drawn so far (rather than simply covering them up).
  • Upgraded Skia to milestone 103

📦 ⟩ v0.9.30 ⟩ Jun 7, 2022

New Features

  • Enhacements to the shared FontLibrary object:
    • Added a reset() method to FontLibrary which uninstalls any fonts that had been dynamically installed via FontLibrary.use()
    • The use() method now checks for previously installed fonts with the same family name (or alias) and will replace them with the newly added font
  • Added pre-compiled binaries for Alpine Linux on arm64

Bugfixes

  • Calling clip with an empty path (or one that does not intersect the current clipping mask) will now prevent drawing altogether
  • Transformation (translate, rotate, etc.) and line-drawing methods (moveTo, lineTo, ellipse, etc.) are now silently ignored if called with NaN, Infinity, or non-Number values in the arguments rather than throwing an error
    • applies to both the Context and Path2D versions of the drawing methods
    • a TypeError is thrown only if the number of arguments is too low (mirroring browser behavior)
  • conicCurveTo() now correctly reflects the canvas's transform state
  • The browser-based version of loadImage() now returns a Promise that correctly resolves to an Image object
  • SVG exports no longer have an invisible, canvas-sized <rect/> as their first element
  • Fixed an incompatibility on Alpine between the version of libstdc++ present on the node:alpine docker images and the version used when building the precompiled binaries

Misc. Improvements

  • Upgraded Skia to milestone 101

📦 ⟩ v0.9.29 ⟩ Feb 7, 2022

New Features

  • PDF exports now support the optional matte argument.

Breaking Changes

  • When the drawImage() function is passed a Canvas object as its image source it will now rasterize the canvas before drawing. The prior behavior (in which it is drawn as a vector graphic) can now be accessed through the new drawCanvas() method which supports the same numerical arguments as drawImage but requires that its first argument be a Canvas.

Bugfixes

  • Regions erased using clearRect() are now properly antialiased
  • The clip() method now interprets the current translate/scale/rotate state correctly when combining clipping masks

Misc. Improvements

  • Upgraded Skia to milestone 97

📦 ⟩ v0.9.28 ⟩ Jan 12, 2022

New Features

  • Added TypeScript definitions for extensions to the DOM spec (contributed by @cprecioso)
  • Added 3D-perspective transformations via the new createProjection() context method
  • Colors can now use the hwb() model

Breaking Changes

  • The Canvas .async property has been deprecated and will be removed in a future release.
    • The saveAs, toBuffer, and toDataURL methods will now be async-only (likewise the shorthand properties).
    • Use their synchronous counterparts (saveAsSync, toBufferSync, and toDataURLSync) if you want to block execution while exporting images.
  • The ImageData constructor now orders its arguments properly: the optional buffer/array argument now comes first

Bugfixes

  • Fixed a stack overflow that was occurring when images became too deeply nested for the default deallocator to handle (primarily due to many thousands of image exports from the same canvas)
  • The source-in, source-out, destination-atop, and copy composite operations now work correctly for paths rather than rendering shapes without color (contributed by @meihuanyu)
  • Shape primitives now behave consistently with browsers when being added to a non-empty path:
    • rect() now issues an initial moveTo rather than extending the path, then leaves the ‘current’ point in its upper left corner
    • ellipse() extends the current path rather than implicitly closing it (contributed by @meihuanyu)
    • arc() also extends the current path rather than closing it

Misc. Improvements

  • Upgraded Skia to milestone 96
  • Added workflow for creating docker build environments

📦 ⟩ v0.9.27 ⟩ Oct 23, 2021

New Features

  • Added pre-compiled binaries for Alpine Linux using the musl C library

📦 ⟩ v0.9.26 ⟩ Oct 18, 2021

New Features

  • Added pre-compiled binaries for 32-bit and 64-bit ARM on Linux (a.k.a. Raspberry Pi)

Bugfixes

  • Windows text rendering has been restored after failing due to changes involving the icudtl.dat file
  • FontLibrary.use now reports an error if the specified font file doesn't exist
  • Fixed a crash that could result from calling measureText with various unicode escapes

Misc. Improvements

  • Upgraded Skia to milestone 94
  • Now embedding a more recent version of the FreeType library on Linux with support for more font formats

📦 ⟩ v0.9.25 ⟩ Aug 22, 2021

Bugfixes

  • Improved image scaling when a larger image is being shrunk down to a smaller size via drawImage()
  • modified imageSmoothingQuality settings to provide a more meaningful range across low, medium, and high
  • measureText() now returns correct metrics regardless of current textAlign setting
  • Rolled back icudtl.dat changes on Windows (which suppressed the misleading warning message but required running as Administrator)

Misc. Improvements

  • Now using Neon v0.9 (with enhanced async event scheduling)

📦 ⟩ v0.9.24 ⟩ Aug 18, 2021

New Features

  • Path2D objects now have a read/write d property with an SVG representation of the path’s contours and an [unwind()][p2d_undwind] method for converting from even-odd to non-zero winding rules
  • The createTexture() context method returns CanvasTexture objects which can be assigned to fillStyle or strokeStyle
  • Textures draw either a parallel-lines pattern or one derived from the provided Path2D object and positioning parameters
  • The marker used when setLineDash is active can now be customized by assigning a Path2D to the context’s lineDashMarker property (default dashing can be restored by assigning null)
  • The marker’s orientation & shape relative to the path being stroked can be controlled by the lineDashFit property which defaults to "turn" but can be set to "move" (which preserves orientation) or "follow" (which distorts the marker’s shape to match the contour)

Bugfixes

  • Removed use of the ?? operator which is unavailable prior to Node 14
  • Prevented a spurious warning on windows incorrectly claiming that the icudtl.dat file could not be found

Misc. Improvements

  • The Path2D simplify() method now takes an optional fill-rule argument
  • Added support for versions of macOS starting with 10.13 (High Sierra)

📦 ⟩ v0.9.23 ⟩ Jul 12, 2021

New Features

  • Conic béziers can now be drawn to the context or a Path2D with the conicCurveTo() method
  • Text can be converted to a Path2D using the context’s new outlineText() method
  • Path2D objects can now report back on their internal geometry with:
    • the edges property which contains an array of line-drawing commands describing the path’s individual contours
    • the contains() method which tests whether a given point is on/within the path
    • the points() method which returns an array of [x, y] pairs at the requested spacing along the curve’s periphery
  • A modified copy of a source Path2D can now be created using:
    • offset() or transform() to shift position or apply a DOMMatrix respectively
    • jitter() to break the path into smaller sections and apply random noise to the segments’ positions
    • round() to round off every sharp corner in a path to a particular radius
    • trim() to select a percentage-based subsection of the path
  • Two similar paths can be ‘tweened’ into a proportional combination of their coordinates using the interpolate() method

Bugfixes

  • Passing a Path2D argument to the fill() or stroke() method no longer disturbs the context’s ‘current’ path (if one has been created using beginPath())
  • The filter property will now accept percentage values greater than 999%

Misc. Improvements

  • The newPage() and saveAs() methods now work in the browser, including the ability to save image sequences to a zip archive. The browser’s canvas is still doing all the drawing however, so file export formats will be limited to PNG and JPEG and none of the other Skia-specific extensions will be available.
  • The file-export methods now accept a matte value in their options object which can be used to set the background color for any portions of the canvas that were left semi-transparent
  • Canvas dimensions are no longer rounded-off to integer values (at least until a bitmap needs to be generated for export)
  • Linux builds will now run on some older systems going back to glibc 2.24

📦 ⟩ v0.9.22 ⟩ Jun 09, 2021

New Features

  • Rasterization and file i/o are now handled asynchronously in a background thread. See the discussion of Canvas’s new async property for details.
  • Output files can now be generated at pixel-ratios > 1 for High-DPI screens. SaveAs and the other canvas output functions all accept an optional density argument which is an integer ≥1 and will upscale the image accordingly. The density can also be passed using the filename argument by ending the name with an ‘@’ suffix like `some-image@2x.png`.
  • SVG exports can optionally convert text to paths by setting the outline argument to true.

Breaking Changes

  • The canvas functions dealing with rasterization (toBuffer, toDataURL, png, jpg, pdf, and svg) and file i/o (saveAs) are now asynchronous and return Promise objects. The old, synchronous behavior is still available on a canvas-by-canvas basis by setting its async property to false.
  • The optional quality argument accepted by the output methods is now a float in the range 0–1 rather than an integer from 0–100. This is consistent with the encoderOptions arg in the spec. Quality now defaults to 0.92 (again, as per the spec) rather than lossless.

Bugfixes

  • measureText was reporting zero when asked to measure a string that was entirely made of whitespace. This is still the case for ‘blank‘ lines when textWrap is set to true but in the default, single-line mode the metrics will now report the width of the whitespace.
  • Changed the way text rendering was staged so that SVG exports didn’t entirely omit(!) text from their output. As a result, Context2Ds now use an external Typesetter struct to manage layout and rendering.

📦 ⟩ v0.9.21 ⟩ May 22, 2021

New Features

  • Now runs on Windows and Apple Silicon Macs.
  • Precompiled binaries support Node 10, 12, 14+.
  • Image objects can be initialized from PNG, JPEG, GIF, BMP, or ICO data.
  • Path2D objects can now be combined using boolean operators and can measure their own bounding boxes.
  • Context objects now support createConicGradient().
  • Image objects now return a promise from their decode() method allowing for async loading without the loadImage helper.

Bugfixes

  • Calling drawImage with a Canvas object as the argument now uses a Skia Pict rather than a Drawable as the interchange format, meaning it can actually respect the canvas's current globalAlpha and globalCompositeOperation state (fixed #6).
  • Improved some spurious error messages when trying to generate a graphics file from a canvas whose width and/or height was set to zero (fixed #5).
  • CanvasPatterns now respect the imageSmoothingEnabled setting
  • The counterclockwise arg to ellipse and arc is now correctly treated as optional.

Misc. Improvements

  • Made the console.log representations of the canvas-related objects friendlier.
  • Added new test suites for Path2D, Image, and Canvas’s format support.
  • Created workflows to automate precompiled binary builds, testing, and npm package updating.

📦 ⟩ v0.9.20 ⟩ Mar 27, 2021

Bugfixes

  • The loadImage helper can now handle Buffer arguments

Misc. Improvements

  • Improved documentation of compilation steps and use of line height with ctx.font

📦 ⟩ v0.9.19 ⟩ Aug 30, 2020

Initial public release 🎉