npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

static-chafa

v1.0.9

Published

Zero-dependency terminal image rendering - static & animated images to ANSI art

Readme

static-chafa

Zero-dependency terminal image rendering. Decodes PNG, JPEG, BMP, GIF, and WebP (static + animated) in native C, renders to ANSI terminal art using the chafa engine, and returns strings to JavaScript. Ships as a Node.js NAPI native addon with platform-specific binaries.

import Chafa, { CanvasMode, DitherMode } from "static-chafa";

const chafa = new Chafa({ termW: 80, termH: 24 });
const { ansi, metrics } = chafa.render(imageBuffer);
process.stdout.write(ansi);
// metrics: { parseMs, drawMs, buildMs, totalMs, imgW/H, canvasW/H, ... }
chafa.destroy();

Features

  • 5 image formats - PNG, JPEG, BMP, GIF, WebP (all decoded in C via libpng/libjpeg/libwebp/stb_image)
  • Animation - GIF and WebP frame-by-frame playback with openAnimation() / next() / renderFrame()
  • Full chafa config - 22 config fields covering canvas mode, pixel mode, dithering, color space, symbol selectors, and more
  • Rich metrics - per-operation timing (parse/draw/build/total), image dimensions, canvas dimensions, RGBA buffer size
  • Decode once, render many - pre-decode to raw RGBA with decode(), render with multiple configs via renderRgba()
  • Cell matrix - access the raw character grid as JSON with renderMatrix()
  • Using keyword - Symbol.dispose on Chafa, ChafaImage, and ChafaAnimation
  • Zero runtime deps - chafa is compiled verbatim with a complete GLib replacement layer (vendor/chafa/glib_mini.h)
  • Cross-platform - linux-x64, linux-arm64, darwin-arm64, win32-x64 via zig cc

Quick Start

bun install static-chafa
import Chafa from "static-chafa";

// Create an instance with your terminal dimensions
const chafa = new Chafa({ termW: 80, termH: 24 });

// Render any supported image
const { ansi, metrics } = chafa.render(fs.readFileSync("cat.png"));
process.stdout.write(ansi);

console.log(metrics);
// {
//   parseMs: 5.4,    // image decode time
//   scaleMs: 0.0,    // pixel-fit pre-scale time (0 when no scaling)
//   drawMs: 2.6,     // chafa symbol matching
//   buildMs: 0.1,    // ANSI string generation
//   totalMs: 8.1,    // sum
//   imgW: 641, imgH: 641,
//   canvasW: 80, canvasH: 24,
//   canvasPw: 640, canvasPh: 192,
//   format: 0, canvasMode: 0, pixelMode: 0, pixelFit: 1
// }

chafa.destroy();

API

new Chafa(config?: Partial<ChafaConfig>)

Creates a rendering instance. Each instance holds its own configuration and caches a single chafa canvas.

import Chafa, { CanvasMode, DitherMode } from "static-chafa";

const chafa = new Chafa({
    termW: 80,          // width in cells
    termH: 24,          // height in cells
    canvasMode: CanvasMode.TRUECOLOR,
    ditherMode: DitherMode.NONE,
    // ...see ChafaConfig for all options
});

Methods

| Method | Returns | Description | |--------|---------|-------------| | render(buffer) | { ansi, metrics } | Decode + render to ANSI string | | renderPath(path) | { ansi, metrics } | Read file + render to ANSI string | | decode(buffer) | ChafaImage | Decode to raw RGBA pixels | | renderRgba(rgba, w, h) | { ansi, metrics } | Render pre-decoded RGBA | | renderMatrix(buffer) | { matrix, metrics } | Decode + render to JSON cell grid (symbol mode only) | | renderMatrixRgba(rgba, w, h) | { matrix, metrics } | Render pre-decoded RGBA to cell grid (symbol mode only) | | openAnimation(buffer) | ChafaAnimation | Open animated GIF/WebP | | openVideo(buffer, decodeW?, decodeH?) | ChafaVideo | Open video file (MP4/MKV/WebM/AVI) | | updateConfig(partial) | void | Update config (invalidates canvas) | | autoDetect(timeoutMs?) | Promise<TerminalInfo> | Probe the terminal and apply capabilities | | info() | { config, features, lastMetrics } | Debug / perf snapshot | | destroy() | void | Free all native resources |

Static utilities:

| Method | Returns | Description | |--------|---------|-------------| | Chafa.detect(timeoutMs?) | Promise<TerminalInfo> | Probe terminal: pixel protocols, cell size, colors | | Chafa.supportedFeatures() | string | CPU features (e.g. "POPCNT") | | Chafa.ansiToHtml(ansi) | string | ANSI art to HTML |

ChafaImage

Holds decoded RGBA pixels. Created by chafa.decode(buffer).

const img = chafa.decode(buffer);
img.width;      // 641
img.height;     // 641
img.rgba;       // Uint8Array of RGBA pixels
img.stride;     // row stride in bytes (width * 4)
img.format;     // 0=PNG, 1=JPEG, 2=BMP, 3=GIF, 4=WebP
img.metrics;    // CodecMetrics from decode

// Re-render with different configs
chafa.updateConfig({ canvasMode: CanvasMode.INDEXED_256 });
chafa.renderRgba(img.rgba, img.width, img.height);

ChafaAnimation

Plays animated GIF/WebP with an integrated player: play() / pause() / goto() plus onFrame() events. Works in every pixel mode - play() renders frames with the owning instance's config (sixels, kitty, symbols), sized to fill the terminal.

const anim = chafa.openAnimation(gifBuffer);
anim.loop = true;
anim.onFrame((frame) => process.stdout.write(`\x1b[H${frame.ansi}`));
anim.play();          // internal timer honors per-frame delays
// ...
anim.pause();         // freeze on the current frame
anim.goto(10);        // jump to frame 10 (emits an onFrame event)
anim.close();

Manual stepping (next() + renderFrame()) is also supported - next() emits onFrame events too. In kitty mode, animation frames reuse a single kitty image id (no per-frame image leak).

ChafaVideo

Plays MP4, MKV, WebM, AVI, and any other container FFmpeg supports. Requires FFmpeg shared libraries on the system (throws a descriptive error if missing). Frames are zero-copy views into the decoder ring buffer.

const video = chafa.openVideo(fs.readFileSync("clip.mp4"));
// Metadata
video.width; video.height; video.durationSec; video.fps;
video.hasAudio; video.audioCodec; video.audioSampleRate; video.audioChannels;

// Integrated player: paced playback + onFrame events
video.onFrame((frame) => {
    process.stdout.write(`\x1b[H${chafa.renderRgba(frame.rgba, frame.width, frame.height).ansi}`);
    // frame.audio is Float32Array PCM (needs videoIncludeAudio: 1)
});
video.play();         // paced to presentation timestamps
video.pause();
video.goto(42);       // seek + return the frame there
const poster = video.thumbnail();  // first frame rendered via current config

// Or iterate:
for await (const frame of video) { /* ... */ }

video.close();

With videoIncludeAudio: 1 in the config, each frame carries frame.audio (interleaved float32 PCM covering the frame's timespan), audioSamples, audioChannels, and audioSampleRate. Audio decoding is off by default to save CPU and memory.

Terminal detection

The constructor auto-detects capabilities from the environment ($TERM, $KITTY_WINDOW_ID, $TERM_PROGRAM, $COLORTERM) - kitty terminals get kitty pixel mode, truecolor terminals get truecolor, etc. Explicit config always wins. For a full active probe (pixel protocols, cell size, terminal dimensions), use Chafa.detect() or chafa.autoDetect().

const info = await Chafa.detect();   // probes via escape sequences
// { pixelMode: 2, cellW: 10, cellH: 20, termW: 120, termH: 40, ... }

Using using (TypeScript 5.2+)

{
    using chafa = new Chafa({ termW: 80, termH: 24 });
    const { ansi } = chafa.render(buf);
    // chafa.destroy() called automatically here
}

ChafaConfig

See typedoc docs for full config reference, or the chafa man page.

Key config fields:

| Field | Type | Default | Description | |-------|------|---------|-------------| | termW, termH | number | 80, 24 | Cell grid dimensions | | cellW, cellH | number | 8, 16 | Cell pixel size (pixel modes only; ignored in symbol mode) | | workFactor | number | 0.0 | Quality/speed (0=fast, 1=best) | | canvasMode | number | 0 | CanvasMode.TRUECOLOR / INDEXED_256 / ... | | pixelMode | number | 0 | PixelMode.SYMBOLS / SIXELS / KITTY / ITERM2 | | pixelFit | number | 1 | PixelFit.NONE (hand pixels to chafa) / SCALE (pre-scale to fill termW × cellW × termH × cellH, default) | | videoIncludeAudio | number | 0 | Decode video audio into per-frame PCM (1 = on) | | videoThreads | number | 0 | FFmpeg decoder thread count (0 = auto) | | swsScale | number | 0 | FFmpeg scaler: SwsScale.AUTO / BILINEAR / POINT / AREA / FAST_BILINEAR | | videoDecodeScale | number | 1.0 | Video decode target as a fraction of the fit size (lower = faster decode, lower quality) | | tuned | number | 1 | Apply tuned per-mode/size defaults (see below). 0 = off | | ditherMode | number | 0 | DitherMode.NONE / ORDERED / DIFFUSION / NOISE | | symbols | string | "" | Chafa CLI selector string (e.g. "block+border+space-wide") | | fillSymbols | string | "" | Fill symbol map selector string |

Tuned defaults

playground/tuner.ts (a multi-hour Bayesian optimizer scoring SSIM*100 - lambda*ms across terminal sizes and media) found per-(pixel mode, terminal size) optimal configs, which are baked into the library as defaults. On construction - and whenever you switch pixelMode, termW, or termH via updateConfig() - fields you haven't set explicitly are filled from these tuned values (numeric fields interpolate smoothly between probed terminal sizes; categorical fields follow the nearest size):

const chafa = new Chafa({ termW: 120, termH: 40 });   // tuned symbol defaults
chafa.updateConfig({ pixelMode: PixelMode.KITTY });    // re-tunes for kitty
chafa.updateConfig({ termW: 240, termH: 72 });         // re-tunes for the size

// Opt out - anything you pass explicitly always wins, and you can disable
// the whole mechanism:
new Chafa({ tuned: 0 });

Inspect or reuse the logic yourself with tunedDefaults(pixelMode, termW, termH).

In pixel modes the output occupies termW × cellW by termH × cellH screen pixels (640×384 by default), filling the same terminal area as symbol mode. With pixelFit: SCALE (default) source pixels are pre-scaled to that area, so chafa draws 1:1; video frames are decoded directly at the fit size, making this free.

CodecMetrics

Returned with every render/decode operation:

| Field | Description | |-------|-------------| | parseMs | Image format detection + codec decode time | | drawMs | chafa_canvas_draw_all_pixels - scaling, symbol matching | | buildMs | chafa_canvas_print - ANSI string generation | | totalMs | parseMs + drawMs + buildMs | | imgW, imgH | Source image pixels | | canvasW, canvasH | Cell grid dimensions | | canvasPw, canvasPh | Internal pixel canvas size | | rgbaBytes | Decoded RGBA buffer size | | format | 0=PNG, 1=JPEG, 2=BMP, 3=GIF, 4=WebP | | canvasMode, pixelMode | Active rendering mode enums | | pixelFit | Active pixel fit strategy | | haveAlpha | Source had alpha channel |

Architecture

Chafa's source tree (39 .c files) is compiled verbatim into the native addon. A vendor layer at vendor/chafa/ replaces GLib entirely with a custom implementation (~750 lines of glib_mini.h), provides a fake autotools config, and supplies stub headers. The include path -I vendor/chafa is placed first so chafa's #include <glib.h> resolves to our replacement.

chafa source           vendor layer          our code
-------------          ------------          --------
internal/*.c           vendor/chafa/         src/codec.c
chafa-*.c   -includes-> config.h   -includes-> src/addon.c
smolscale/*.c           glib_mini.h           src/index.ts
                        chafa_quarks.c

Image decode is handled by our own code in codec.c using libpng, libjpeg (IJG), libwebp, and stb_image (GIF). Chafa receives decoded RGBA pixels via chafa_canvas_draw_all_pixels().

Development

bun run build:dev     # Build codec.so for Bun FFI
bun run build:napi    # Build .node addon
bun run test          # Run 38 tests
bun run bench         # Run comprehensive benchmark
bun run docs          # Generate API docs
bun run playground    # Interactive test suite

Tuning harness

playground/harness.ts renders media frames with full per-stage timings and pixel-compares every output against ground truth (PSNR/SSIM), emitting PNGs (ground truth, reference, symbols raster, decoded sixel/kitty) plus metrics.json:

bun run playground/harness.ts render fox.png --mode all --frames 10
bun run playground/harness.ts tune fox.png --frames 5        # ranked 1-D sweep

playground/tuner.ts runs a long-horizon automatic optimizer: 15 concurrent processes (5 terminal sizes x symbols/sixel/kitty) continuously sampling the full chafa + FFmpeg config space (workFactor, dithering, color space, canvas mode, symbol maps, alpha/bg handling, pixelFit, decode scale, decoder threads, swscale filter, ...) with a TPE (Bayesian) sampler. It scores SSIM*100 - lambda*ms, checkpoints state (resumable), shows a live status page, and finally writes best_configs.json per size/mode plus formulas.json (term-size -> config-value fits):

bun run playground/tuner.ts            # 8h budget, live status page
bun run playground/tuner.ts --resume   # continue from checkpoint
bun run playground/tuner.ts --silent --hours 0.01   # quick smoke test

Maintenance

When chafa updates, only the vendor layer needs expansion (see VENDOR.md):

  • New .c files -> add to CHAFA_FILES in build.sh
  • New GLib includes -> create stub headers
  • New GLib functions -> implement in glib_mini.h

License

MIT