# PureImage: a Canvas 2D implementation with no native dependencies

> PureImage is a pure JavaScript implementation of the HTML Canvas 2D drawing API for Node, written in TypeScript and shipping as CommonJS, ES modules and a browser build. Its author is explicit that it values portability and simplicity over speed, and it replaces node-canvas on machines where a Cairo toolchain is not an option.

**joshmarinacci/node-pureimage** — Pure JS implementation of the HTML Canvas 2D drawing API

- Repository: https://github.com/joshmarinacci/node-pureimage
- Stars: 842 · Forks: 86
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/joshmarinacci-node-pureimage

## A Canvas API without Cairo underneath

PureImage is described as a pure, 100 percent JavaScript implementation of the HTML Canvas 2D drawing API for NodeJS, and the first line of the README insists on the point: it has no native dependencies.

That matters more than it first appears. The dominant Node drawing library is node-canvas, and the author explains why they wrote a replacement instead of using it. The popular Canvas module does a great job, but it is backed by Cairo, a C and C++ layer. Native dependencies in Node modules often fail to compile, break after a system update, and frequently do not support non-x86 architectures such as the Raspberry Pi. You also need a compiler installed already, along with the other native dependencies such as Cairo itself.

The stated goal is to implement the HTML Canvas spec in a headless Node buffer, with no browser and no window required. The purpose is a small, maintainable Canvas library.

So the use cases are the ones where a browser is not available and a compiler is not welcome: generating an image on a server, resizing an uploaded picture, drawing a text label, rendering a badge, converting to grayscale. Anything you could do with the standard Canvas 2D API, plus extra APIs to save an image as PNG or JPEG.

The design rule stated in the README is worth memorising before you pick it. PureImage values portability and simplicity of implementation over speed, and if two algorithms would do, it takes the one with the simplest implementation and preferably the fewest lines. Special cases and optimisations are avoided to keep the code simple and maintainable, so it should run everywhere and produce the same output. It will not be fast.

## Installing and what changed in the TypeScript rewrite

Installation is the usual npm line.

```bash
npm install pureimage
```

As of version 0.4 the library has been rewritten in 100 percent TypeScript, and the module is compiled to both CommonJS and ES modules. The README's promise to existing users is that if it was working before it should still work, and the instruction if it is not is to file a bug report.

There is one behavioural change the README calls out, because it breaks code at the call site. The font loading function now returns a promise instead of using a callback. If you need synchronous behaviour, a synchronous variant is still available, and the README says you can still use it.

That is the sort of change that a rewrite can introduce quietly, so it is worth checking if you have code that loads fonts inside a synchronous setup routine.

The dependency list is three pure JavaScript packages, which is the whole point of the design. OpenType handles font parsing, PngJS handles PNG import and export, and jpeg-js handles JPEG import and export. Nothing in that list needs a compiler, and nothing needs a system library, which is exactly what makes the package install cleanly on a Raspberry Pi or inside a scratch container.

## A first drawing and writing a PNG file

The simplest example in the README makes a 100 by 100 image, fills it red and writes it to a PNG file.

```javascript
import * as PImage from "pureimage";
import * as fs from "fs";

// make image
const img1 = PImage.make(100, 100);

// get canvas context
const ctx = img1.getContext("2d");

// fill with red
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 100);

//write to 'out.png'
PImage.encodePNGToStream(img1, fs.createWriteStream("out.png"))
  .then(() => {
    console.log("wrote out the png file to out.png");
  })
  .catch((e) => {
    console.log("there was an error writing");
  });
```

The API is the browser API, deliberately. A canvas is created with make, a drawing context is obtained with getContext, and the context carries fillStyle, font and fillText exactly as in the browser. The one addition is the encoding call, which takes the canvas and a writable stream and returns a promise.

The examples section fills in the rest of the surface. A new image is filled with fully opaque black by default. Fill and stroke support rectangles, lines, quadratic curves, bezier curves and arcs, and paths are built with beginPath, arc and closePath before being filled. Copying and scaling use nearest neighbour interpolation. Text is drawn with fillText after registering a font, and images are read and written as PNG and JPEG through streams that return promises.

The example that reads a JPEG, resizes it and writes it back is the one that shows the practical shape of the library, since it is the operation a server actually performs on an uploaded image.

## What works and what is still on the roadmap

The README separates the implemented features from the missing ones, and the split is the honest assessment of what you can rely on.

Supported: setting pixels, stroking and filling paths across the shape list above, copying and scaling images with nearest neighbour, importing and exporting JPEG and PNG from streams using promises, rendering basic text, anti-aliased strokes and fills, transforms, the standard globalAlpha and rgba alpha compositing, and clipping shapes.

On the roadmap but still missing: gradient fills, image fills, blend modes other than source over, smooth clip shapes, bold and italic fonts, and smooth image interpolation.

Two of those deserve attention because they are the ones people trip over. Image interpolation is nearest neighbour only, so scaling a photo up will look blocky, and smooth interpolation is not coming. And there is no bold or italic, so a design that needs weights will have to register separate font files, which is a workaround rather than a feature.

The README also notes that the circle example in its own documentation produces an image with what it calls fringing bugs, and the repository carries a firstimages directory with those early renders. That is a useful signal about the maturity of the edge cases, and it sits next to the anti-aliasing claim, so a test on your own data is worthwhile.

## Text, fonts and the streaming workflow

Font handling is the part with the most moving pieces, because a headless renderer has to load a font file itself rather than rely on the system.

A font is registered with a path to a font file and a name, and the README example registers Source Sans Pro from disk and then sets it as the context font.

```js
test("font test", (t) => {
  var fnt = PImage.registerFont(
    "test/fonts/SourceSansPro-Regular.ttf",
    "Source Sans Pro",
  );
  fnt.loadSync();
  var img = PImage.make(200, 200);
  var ctx = img.getContext("2d");
  ctx.fillStyle = "#ffffff";
  ctx.font = "48pt 'Source Sans Pro'";
  ctx.fillText("ABC", 80, 80);
});
```

The synchronous load variant is the one used here, which is a hint about how the promise change affects scripts that run assertions in sequence.

The streaming examples cover the other common shape, pulling an image from a URL, decoding it, drawing the current date on it in a large font and writing the result to disk. The README shows the callback version and then says the same thing can be done with promises, and the examples directory contains separate files for the synchronous, asynchronous and promise variants, so you can pick the style that matches the rest of your code.

The write path takes a quality argument for JPEG, so the encoder can be told how much compression to apply, and the output path is a normal Node writable stream, which means it composes with anything that accepts one.

## Package shape, build scripts and maintenance

The package metadata tells you how the library is consumed and how it is maintained.

The published package is version 0.4.20, marked as an ES module package with types pointing at the generated declarations, a module entry for the ES module build, a main entry for the CommonJS build, and a separate browser field for the browser bundle. The files array ships only the dist directory, so the published tarball is the compiled output rather than the TypeScript source.

The three runtime dependencies are the ones already mentioned, and the development dependencies are esbuild for the bundling, TypeScript itself, npm-dts for the declaration files, and vitest for the tests. The scripts show the workflow: a build script runs the build file, a type-check script runs the compiler, the test script runs vitest and then the type check, and the release script chains clean, build, test, a patch version bump and publish.

```json
  "scripts": {
    "test": "npm run vitest && npm run type-check",
    "build": "node build.js",
    "type-check": "tsc",
    "release": "npm run clean && npm run build && npm run test && npm version patch && npm publish"
  },
  "engines": {
    "node": ">=14.19.0"
  },
```

That release script is a plain patch bump, so the published versions move one patch at a time and there is no semantic versioning story to rely on for breaking changes.

The repository also carries an .esdoc.json for API documentation, a Trunk configuration for linting, a .nvmrc, an examples directory and a test directory. The tags on the repository are unusual: stable, test-v1 and test-v2, all dated 2023-10-18, and described in the tag messages as experiments with the release process. The last push was on 2026-07-13, so the code is maintained, but the absence of numbered tags means you should pin a published npm version rather than a git reference.

The licence is MIT, so the code can be used in commercial projects without the copyleft obligations of a GPL library.

## Conclusion

PureImage is the right choice when a Node service needs to draw something, resizing an image or stamping a badge in a container that has no compiler and no Cairo, and the author has measured the trade in public by listing what is missing: gradients, image fills, blend modes beyond source over and bold text. It is the wrong choice for anything on a hot path, because the README says outright that it is not meant to be fast and that you should use a native backed library if speed matters. Verify first that the Canvas features your drawing needs are on the supported list rather than the roadmap list, and pin the version you depend on, since the release tags on the repository are test tags rather than a numbered release series.

## FAQ

### What is PureImage and what does it replace?

PureImage is a pure JavaScript implementation of the HTML Canvas 2D drawing API for Node, created as an alternative to node-canvas, which is backed by Cairo and requires a native toolchain.

### How do I install PureImage?

Run npm install pureimage. Since version 0.4 the library is written in TypeScript and compiled to both CommonJS and ES modules, and its only runtime dependencies are jpeg-js, opentype.js and pngjs.

### Which Canvas features does PureImage not support yet?

Gradient fills, image fills, blend modes other than source over, smooth clip shapes, bold and italic fonts, and smooth image interpolation are listed on the roadmap as still missing. Scaling uses nearest neighbour only.

### Did the font loading API change?

Yes. The README notes that font loading now returns a promise instead of using a callback, and that a synchronous variant is still available if you need it.

## Sources

- [Issues](https://github.com/joshmarinacci/node-pureimage/issues)
- [joshmarinacci/node-pureimage on GitHub](https://github.com/joshmarinacci/node-pureimage)
- [License: MIT](https://github.com/joshmarinacci/node-pureimage/blob/master/LICENSE)
- [README](https://github.com/joshmarinacci/node-pureimage/blob/master/README.md)
- [Releases](https://github.com/joshmarinacci/node-pureimage/releases)

---

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