Package

purescript-puregrain

Repository
roman-mkh/puregrain
License
MIT
Uploaded by
pacchettibotti
Published on
2026-10-16T21:52:25Z

CI Pursuit License: MIT

Error-diffusion and ordered dithering in PureScript: Floyd–Steinberg, Atkinson, Jarvis–Judice–Ninke and Bayer, for grayscale and color images, down to a few gray levels, a few levels per color channel, or a fixed palette such as CGA or the Commodore 64. It comes with a command-line tool for PNG images.

Status: first release (0.x). The library works and is tested. Before 1.0, a breaking change raises the minor version (0.1 → 0.2); CHANGELOG.md lists the changes. API reference: Pursuit. Not on npm yet.

What it does

  • Error diffusion with a choice of kernel: Floyd–Steinberg, Atkinson, Jarvis–Judice–Ninke, or your own (a list of neighbour offsets and weights, checked when you build it).
  • Grayscale and color. For color there are two approaches:
    • dither each channel on its own, e.g. 4 levels per channel;
    • or pick the nearest color from a palette, looking at the whole pixel.
  • Ordered (Bayer) dithering: Bayer matrices of any power-of-2 size, or your own threshold map; on its own, or combined with error diffusion. See docs/ordered-dithering.md.
  • Ready-made palettes: black & white, web-safe, CGA/EGA/VGA, ANSI 16/256 (xterm), Commodore 64, ZX Spectrum. Every palette's values are checked against a cited source. See docs/palettes.md.
  • Machine screens: the real look of 8-bit machines whose cells allow only a few colors, starting with the ZX Spectrum's two colors per 8×8 cell, with ordered dithering or error diffusion inside the cells. Written as a PNG, or as the Spectrum's own screen file and a tape that loads it. See docs/machine-screens/.
  • In memory or as a stream: dither a whole image at once, or a lazy stream of rows, each dithered only when it's needed, so only the rows in flight are in memory and the input can even be endless. Or go one row at a time from any source, a network stream for example, with your own loop.
  • Time grows linearly with the pixel count (measured up to 1024×1024). See docs/benchmarks-dithering.md.

When to use it, and when not

Use it when:

  • You need dithering inside a PureScript program. Pure functions, written entirely in PureScript, with no native dependencies: it runs wherever JavaScript runs (Node, browsers).
  • Images are large or streamed. Rows are dithered one at a time, so memory stays small, and the source can be a file or a network stream.

Look elsewhere when:

  • Speed matters. It runs as plain JavaScript; timings per mode and kernel are in the benchmarks. For batches of large images or interactive use, a native tool such as ImageMagick is the better choice.
  • You need the best photo quality. No gamma-correct (linear-light) diffusion, no perceptual color distance and no serpentine scanning yet (see Limitations).

Getting started

Setting up PureScript

You need Node.js, the PureScript compiler and spago. The official Getting Started guide installs them; spago's documentation covers the current spago (the guide still shows the older spago.dhall files). Tools installed locally, with npm install --save-dev purescript spago, run through npx: npx spago init.

In your PureScript project

spago init                # only for a new project
spago install puregrain

If spago doesn't find puregrain, your project's package set is older than puregrain; spago upgrade moves the project to the latest set.

A first program: a small gray gradient, made in code, and the same gradient dithered to black and white with Floyd–Steinberg, both printed as text.

import Puregrain as P

-- | A small gray image: 8 rows of 32 pixels, from black (0.0) on the left
-- | to white (255.0) on the right.
gradient :: Array (Array Number)
gradient = Array.replicate 8 (map (\x -> toNumber x * 255.0 / 31.0) (Array.range 0 31))

main :: Effect Unit
main = do
  log "The original gradient (shown with 5 shades):"
  log (render gradient)
  log ""
  log "Dithered to black and white with Floyd-Steinberg:"
  log (render (P.ditherImage P.floydSteinberg (P.threshold 128.0) gradient))

ditherImage takes a kernel (where each pixel's rounding error goes), a quantizer (which values a pixel may take) and the image: an array of rows, each an array of pixels. Here a pixel is a gray Number from 0.0 (black) to 255.0 (white); for color, it's P.RGB. render turns an image into text; the whole program, ready to run, is QuickStart.purs. To compile it in your own project, also install the packages it imports: spago install arrays integers maybe strings. Run it with spago run -m Puregrain.Examples.QuickStart (in this repository, see examples/) and your terminal shows the gradient twice: in 5 shades of gray, then dithered to just black and white, as dots whose density follows the gray. The examples also compare more ways to dither the same gradient, and show how to try your own variations in the REPL.

The library works on arrays of pixels; reading and writing image files is up to your application (the command-line tool below does it in Node, with pngjs).

The command-line tool

Dithers PNG images, without writing code. It isn't on npm yet; run it from a checkout of this repository (needs Node.js, developed with v22):

npm install     # the PureScript compiler, spago, pngjs
npm run build   # compiles the library, the tool and the examples
npm run dither-cli -- photo.png dithered.png --palette c64

More examples, every option, and how gray and color output are chosen: cli/README.md.

Working on this repository

After npm install and npm run build:

npm test                  # the library's and the tool's test suites
npm run check             # what CI runs: strict build, tests, end-to-end checks
npm run generate-images   # test images in samples/

How work is done (branches, CI, releases): CONTRIBUTING.md.

Repository layout

Path What
src/, test/ The library (Puregrain.* modules; Puregrain.Internal.* are internal) and its tests
cli/ The command-line tool, a separate package that uses the library
examples/ Small example programs, among them the quick start above; a separate package
scripts/ Test-image generator, benchmark, benchmark chart, end-to-end CLI checks
.github/workflows/ CI: build, tests and end-to-end checks on every push; on version tags also the release checks and the GitHub Release
docs/ Documentation (below)

The three packages live in one spago workspace and are built together.

Documentation

Document Contents
examples/README.md The example programs: what each shows, how to run them, the REPL
cli/README.md The command-line tool: quick start, options, examples
docs/ordered-dithering.md Ordered (Bayer) dithering: thresholds, the matrices, combining it with error diffusion
docs/palettes.md The preset palettes: values, sources, equivalences
docs/machine-screens/ Machine screens: dithering with cells, which mode for which picture; one page per machine, the ZX Spectrum first
docs/test-images.md The generated test images, and what to look for in the results
docs/benchmarks-dithering.md Current performance, per mode and kernel
docs/benchmarks-fifo.md History: how an O(N³) slowdown was found and fixed
CLAUDE.md Design decisions and their reasons (kept up to date for the AI assistant used in development)
CHANGELOG.md Changes to the library, per version
CONTRIBUTING.md How work is done: setup, branches, CI, versions, releases
TODO.md Open work and ideas

License

MIT: use it in any project, open or closed, free or commercial; keep the copyright and license notice. See LICENSE.