# node-canvas: server-side Canvas backed by Cairo, now shipping through a Zig build

> Automattic's Node implementation of the Web Canvas API, giving server-side code text rendering, images, PDF and SVG output, distributed as prebuilt platform binaries with v4 in pre-release behind the canvas npm package.

**Automattic/node-canvas** — Node canvas is a Cairo backed Canvas implementation for NodeJS.

- Repository: https://github.com/Automattic/node-canvas
- Stars: 10,693 · Forks: 1,195
- Language: JavaScript
- License: not declared
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/automattic-node-canvas

## Installing canvas, and the difference between v3 and v4

The npm package is called `canvas` even though the project is node-canvas, which trips up anyone searching for it by repository name. The README gives two install commands, and which one you run decides what you get:

```bash
$ npm install canvas
```

That is v3, the current stable release. The other one:

```bash
$ npm install canvas@next
```

installs v4, which the README describes as still in the pre-release phase while asking for feedback. The v4 changes it lists are specific enough to judge: it fixes a set of font-related problems, adds worker thread support, reduces binary size, removes the post install script, and expands the set of platforms with prebuilt binaries. Removing the post install script is the change most likely to affect a deployment pipeline, since scripts that run on install are a common cause of locked-down build environments failing.

The minimum Node version is 22, which applies to both lines. Prebuilt binaries ship for macOS on x64 and arm64, Linux on x64, x86 and arm64 with either glibc or musl, plus Linux arm and riscv64 on glibc, and Windows on x64, x86 and arm64. Anything outside that list means building from source, and the README's instruction is to change into the package directory and run `zig build`, possibly after tweaking `build.zig`.

## Drawing text and an image, then getting a buffer out

The quick example is a real drawing rather than a placeholder: a 200 by 200 canvas, a rotated 30px Impact fillText call, an underline computed from `measureText`, and a loaded JPEG drawn on top.

```javascript
const { createCanvas, loadImage } = require('canvas')
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d')

// Write "Awesome!"
ctx.font = '30px Impact'
ctx.rotate(0.1)
ctx.fillText('Awesome!', 50, 100)
```

Three details are doing the work. `ctx.font` takes the same CSS font string a browser uses. `measureText` returns the rendered width, so the underline length follows the text rather than being hard-coded. And the final line of the example emits the result as a data URL through `canvas.toDataURL()`.

Creating a canvas of a different type is a constructor argument:

```js
const { createCanvas } = require('canvas')
const mycanvas = createCanvas(200, 200)
const myPDFcanvas = createCanvas(600, 800, 'pdf') // see "PDF Support" section
```

Passing `'pdf'` gives a multi-page document instead of a bitmap, which is how the same code path produces a PDF. The examples directory shows how far that goes, with `multi-page-pdf.js`, `pdf-images.js`, `pdf-link.js` and `small-pdf.js` sitting alongside image work such as `resize.js`, `crop.js`, `gradients.js`, `fill-evenodd.js` and `ray.js`, plus text rendering with `pango-glyphs.js` and `image-caption-overlay.js`.

## The Font API, where most of the real work happens

Text rendering is the part of a canvas library that has to reconcile a web API with system facilities, and the Font API is where that shows. You construct a `FontFace` from a file path or a TrueType buffer, with a family name and optional weight and style descriptors, and add it to the `fonts` set:

```js
const { createCanvas, FontFace, fonts } = require('canvas')
fonts.add(new FontFace('Comic Sans', '/home/you/comicsans.ttf'))

const canvas = createCanvas(500, 500)
const ctx = canvas.getContext('2d')

ctx.font = '12px "Comic Sans"'
ctx.fillText('Everyone hates this font :(', 250, 10)
```

Only `family` is required; `weight` and `style` default to `normal`, mirroring the `@font-face` CSS rule. The `fonts` object implements a subset of the browser's `document.fonts`, with `add`, `has`, `clear`, `delete`, a size and an iterator.

`clear()` is the method that matters in tests. The README's own example is a test suite that registers a font in each test and calls `fonts.clear()` in an `afterEach` hook, because registered fonts otherwise leak between test cases and make rendering depend on execution order. If you render images in CI, that pattern is worth copying from the start.

The v4 announcement also points at this area: a set of font-related fixes is listed as one of the main reasons to try the pre-release. That is a reasonable signal that fonts are still the source of most differences between what a browser draws and what this library draws.

## Non-standard APIs that a browser does not have

The README positions node-canvas as an implementation of the Web Canvas API that follows it as closely as possible, and sends you to the Mozilla documentation for the standard API. For compliance detail it points at a Compatibility Status wiki. The library's own additions are listed separately, which is where you look when you need something the browser cannot do.

The additions worth knowing about are the output methods and the image setters. `Canvas#toBuffer()` is the one you will reach for most, since it gives you PNG bytes without the base64 detour that `toDataURL()` imposes. There are also streaming variants, `createPNGStream()`, `createJPEGStream()` and `createPDFStream()`, which avoid holding a large document in memory when the output is large.

On the image side, `Image#src` accepts a `data:` URI or a remote URL as in a browser, and the README documents the extension that lets you set `src` to a local Buffer instead. `Image#dataMode` controls how pixel data is interpreted.

There are rendering knobs that exist nowhere else: `patternQuality` and `quality` on the context, `textDrawingMode`, an `antialias` property, and a `globalCompositeOperation` value of `saturate` that is not part of the standard list. These are the surface area to check when output does not match what the same code produces in a browser, and they are also the surface area most likely to change between major versions.

## A Zig build, platform packages and worker threads

The v4 build system is the most visible structural change. The repository tree carries `build.zig` and `build.zig.zon`, and `package.json` wires the Zig build into the npm scripts: `prebenchmark` and `pretest-server` both run `zig build`, then `benchmark` runs `node benchmarks/run.js` and `test-server` runs `node test/server.js`.

The `package.json` also shows how binaries are distributed. Instead of compiling on the consumer's machine, platform packages are listed under `optionalDependencies`, with entries such as `canvas-darwin-arm64` and `canvas-linux-x86-musl` all pinned to the same `4.0.0-rc3` version as the package itself. That pin is the point: a mismatched native binary against a different JS version is a segfault rather than an exception, so the two versions have to move together. The `workspaces` field points at `./npm/*`, where those platform packages live, and `files` includes `build.zig`, `src/`, `lib/`, `pkg/` and `util/` so a source build is possible from an installed package.

Worker thread support in v4 follows from the same direction. Canvas drawing is CPU-bound native work, and without workers a render blocks the event loop, which is painful in a web service. Testing also runs through Mocha against `test/*.test.js`, with separate scripts to generate and run the web platform tests, `generate-wpt` and `test-wpt`, so conformance against the browser API is measured rather than assumed. There is also a `tsd` script for the TypeScript definitions in `index.d.ts`, and a `standard` lint pass over the JavaScript.

## Maintenance state, security history and the licensing gap

The repository is not archived and the last push was on 2026-08-24, with the published releases all in the v3.2.x line: v3.2.3 on 2026-03-31, v3.2.2 on 2026-03-21 and v3.2.1 on 2026-01-10. The current `package.json` version is `4.0.0-rc3`, which lines up with the pre-release line the README describes.

Those three releases are short and they are mostly fixes, which is the useful part. v3.2.2 fixed a dangling env pointer in image MIME data cleanup and a `ctx.direction` bug where save and restore did not affect it, preserved the remaining pages of a PDF when width and height changed, and included several security fixes for untrusted input to `getImageData` and `putImageData`. v3.2.1 rejected `loadImage` when the src was null or invalid, fixed `roundRect` when radii were large relative to the rectangle, and fixed compilation on GCC 15. v3.2.3 was a single fix for building with gcc.

Two things follow. If you feed node-canvas images that come from users, the security fixes in 3.2.2 are the reason to make sure you are on it, because `getImageData` and `putImageData` are exactly the entry points reachable from untrusted content. And the gcc entries tell you the C toolchain is a real part of the support surface, which the Zig move in v4 is meant to reduce.

One caveat worth flagging: the repository metadata reports no licence rather than naming one. There is no `LICENSE` file in the tree listing. For a native dependency that most teams install as a transitive package, that is a question worth asking before it lands in a product, and the answer belongs to whoever owns licensing policy at your organisation rather than to this article.

## Conclusion

node-canvas is the answer when a Node service has to render text into pixels, which is why it sits underneath chart renderers and PDF producers rather than beside them. Adopt v3 today, since `npm install canvas` still resolves to the stable line, and treat `canvas@next` as something to evaluate on a scratch branch rather than a dependency to upgrade to. The compatibility wiki is the page that tells you whether the API call you need exists, the FontFace API is the part you will spend the most time on, and the v3.2.x release history shows the project treats untrusted image input as an attack surface.

## FAQ

### What does canvas mean when people say it in coding?

Usually the HTML Canvas API, the browser drawing surface you get from a canvas element and a 2d context, with methods like `fillText`, `drawImage` and `fillRect`. node-canvas is a Node implementation of that same API, so server-side code can use the method names and code paths it would use in a browser, with Cairo doing the drawing instead of the browser.

### Is Canvas an API?

It is, and node-canvas implements it. The README describes the project as an implementation of the Web Canvas API that follows it as closely as possible, and points readers to the Mozilla Canvas API documentation for the standard surface. The project's own Compatibility Status wiki tracks where compliance stands, and the additions such as `toBuffer()`, the PNG, JPEG and PDF stream methods and the FontFace API are explicitly non-standard.

### Why is npm installing two different versions of node-canvas?

Because v4 is still a pre-release. `npm install canvas` gives the stable v3 line, and `npm install canvas@next` gives v4, which the README says fixes font issues, adds worker thread support, reduces binary size, removes the post install script and widens the prebuilt platform list. Prebuilt binaries are separate platform packages pinned to the same version, so a mismatch between the main package and a native binary will crash rather than raise an error.

## Sources

- [Automattic/node-canvas on GitHub](https://github.com/Automattic/node-canvas)
- [Issues](https://github.com/Automattic/node-canvas/issues)
- [README](https://github.com/Automattic/node-canvas/blob/master/README.md)
- [Releases](https://github.com/Automattic/node-canvas/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/automattic-node-canvas
