canvas
canvas, usually called node-canvas, implements much of the browser Canvas 2D API in Node.js using the native Cairo and Pango graphics stack. It can draw shapes, paths, text, gradients, pixels, and loaded images, then encode PNG, JPEG, PDF, SVG, data URLs, or streams. It adds server-specific tools for font registration, image buffers, multipage PDFs, output metadata, and Cairo rendering controls. This is a native Node addon, not a small browser canvas polyfill, and supported-platform binaries determine whether installation is one command or a system-library project.
node-canvas is the established choice when server code genuinely needs the Canvas 2D drawing model. Do not install it for routine image transformations, and confirm your deployment has a prebuilt binary or budget the native Cairo toolchain before committing.
Use it if
- You already know the browser Canvas 2D API and need the same drawing model in Node
- You generate charts, social cards, certificates, image snapshots, or simple PDFs on the server
- Accurate text layout through Cairo and Pango matters more than having a pure JavaScript install
- A test or rendering tool expects the canvas package specifically, as jsdom and related libraries often do
- You only resize, crop, convert, or compress existing images: Sharp has a better pipeline API and avoids manually drawing everything onto a canvas
- You deploy on Alpine Linux, an unsupported CPU, or another platform without a supplied binary and cannot maintain Cairo, Pango, compiler, and optional codec packages in the image
- You need a browser dependency: this npm package is centered on a native Node addon, and Bundlephobia is not meaningful for its Cairo binary stack
- You accept arbitrary dimensions or remote image URLs from users without limits: large surfaces and decoded images can exhaust memory, and server-side URL loading needs the same SSRF controls as any other fetch
Setup reality
npm install canvas downloads prebuilt binaries only for macOS x64 or arm64, glibc Linux x64, and Windows x64. Everything else compiles from source and needs Cairo plus Pango; JPEG, GIF, and SVG input add optional system libraries. Node 18.12+ is required, with the current engine range preferring 20.9+ outside Node 18. Font availability changes output across machines, so register and ship fonts for deterministic renders.
Patterns
Draw and encode a PNGdraw-png
const { createCanvas } = require('canvas')
const canvas = createCanvas(640, 360)
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#0f172a'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#fff'
ctx.font = 'bold 42px sans-serif'
ctx.fillText('Server-rendered image', 40, 190)
const png = canvas.toBuffer('image/png')toBuffer without a callback encodes synchronously, so large renders can block the Node event loop.
Register a bundled font before drawingregister-custom-font
const { registerFont, createCanvas } = require('canvas')
registerFont('./fonts/Inter-Bold.ttf', {
family: 'Inter',
weight: '700',
})
const canvas = createCanvas(800, 400)
const ctx = canvas.getContext('2d')
ctx.font = '700 48px Inter'
ctx.fillText('Consistent typography', 40, 100)Register fonts before creating dependent text layouts and ship the licensed font file with every deployment.
Load and crop an imageload-local-image
const { createCanvas, loadImage } = require('canvas')
const image = await loadImage('./input/photo.jpg')
const canvas = createCanvas(400, 400)
const ctx = canvas.getContext('2d')
ctx.drawImage(image, 100, 0, 400, 400, 0, 0, 400, 400)
const output = canvas.toBuffer('image/png')JPEG loading only works when the installed binary was built with JPEG support; source builds need the JPEG development library.
Validate a remote image URL before loadingconstrain-remote-image
const source = new URL(userSuppliedUrl)
if (source.protocol !== 'https:' || !allowedHosts.has(source.hostname)) {
throw new Error('image source is not allowed')
}
const image = await loadImage(source.href)
ctx.drawImage(image, 0, 0, 300, 200)loadImage accepts remote URLs. Enforce host, protocol, redirect, response-size, and image-dimension limits outside this library.
Encode a JPEG with quality controlsencode-jpeg
const jpeg = canvas.toBuffer('image/jpeg', {
quality: 0.85,
progressive: true,
chromaSubsampling: false,
})JPEG drops transparency and requires JPEG support in the native build; paint an explicit background before encoding.
Stream PNG output to a filestream-png
const fs = require('node:fs')
const { pipeline } = require('node:stream/promises')
await pipeline(
canvas.createPNGStream({ compressionLevel: 6 }),
fs.createWriteStream('output.png'),
)Streaming avoids holding the encoded output buffer all at once, though the drawing surface itself remains in memory.
Read and modify pixel dataedit-pixels
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
for (let i = 0; i < imageData.data.length; i += 4) {
const gray = Math.round(
imageData.data[i] * 0.299 +
imageData.data[i + 1] * 0.587 +
imageData.data[i + 2] * 0.114
)
imageData.data[i] = gray
imageData.data[i + 1] = gray
imageData.data[i + 2] = gray
}
ctx.putImageData(imageData, 0, 0)Per-pixel JavaScript loops are expensive for large images; Sharp is usually faster for standard filters.
Generate a multipage PDFcreate-multipage-pdf
const pdf = createCanvas(595, 842, 'pdf')
const ctx = pdf.getContext('2d')
ctx.textDrawingMode = 'glyph'
ctx.font = '24px Helvetica'
ctx.fillText('Page one', 50, 80)
ctx.addPage()
ctx.fillText('Page two', 50, 80)
const buffer = pdf.toBuffer('application/pdf', {
title: 'Two page report',
author: 'Example service',
})Glyph text mode makes PDF text selectable and smaller; metadata needs Cairo 1.16 or newer.
Stream a PDF responsestream-pdf
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', 'inline; filename=report.pdf')
pdf.createPDFStream({ title: 'Report' }).pipe(res)Create the canvas with type pdf first. Streaming reduces encoded-buffer memory but drawing work still happens in process.
Create SVG outputrender-svg
const fs = require('node:fs')
const svg = createCanvas(600, 200, 'svg')
const ctx = svg.getContext('2d')
ctx.fillStyle = '#2563eb'
ctx.fillRect(10, 10, 580, 180)
ctx.fillStyle = '#fff'
ctx.font = '32px sans-serif'
ctx.fillText('Vector output', 180, 115)
fs.writeFileSync('output.svg', svg.toBuffer())Drawing an SVG image onto a canvas rasterizes that source; it does not preserve the source SVG structure.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| skia-canvas | npm | You want a Canvas-like Node API backed by Skia with a different prebuilt-binary matrix |
| sharp | npm | Your work is image resizing, compositing, conversion, metadata, and efficient pipelines rather than freeform Canvas drawing |
| pureimage | npm | You need a pure JavaScript Canvas-style renderer and can accept lower fidelity and a smaller feature set |