Skip to content

Latest commit

 

History

History
184 lines (141 loc) · 6.05 KB

File metadata and controls

184 lines (141 loc) · 6.05 KB

@opendisplay/epaper-dithering

npm

High-quality dithering algorithms for e-paper/e-ink displays, implemented in TypeScript. Works in both browser and Node.js environments.

Features

  • 9 Dithering Algorithms: From fast ordered dithering to high-quality error diffusion
  • 8 Color Schemes: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE_4/8/16
  • Measured Palettes: Use real display-calibrated colors for accurate dithering (SPECTRA_7_3_6COLOR, BWRY_3_97, and more)
  • LCH Color Matching: Perceptual LAB color space with hue-weighted distance — hue errors can't be recovered by error diffusion, so they're prioritized
  • Serpentine Scanning: Alternates row direction to eliminate directional artifacts
  • Universal: Works in browser (Canvas API) and Node.js (with sharp/jimp)
  • Zero Dependencies: Pure TypeScript, no image library dependencies
  • Fast: 256-entry sRGB LUT, pre-computed palette LAB arrays, typed array pixel buffers

Installation

npm install @opendisplay/epaper-dithering
# or
bun add @opendisplay/epaper-dithering

Quick Start

Browser (Canvas API)

import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';

const img = new Image();
img.src = 'photo.jpg';
await img.decode();

const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);

const dithered = ditherImage(
  { width: imageData.width, height: imageData.height, data: imageData.data },
  ColorScheme.BWR,
  DitherMode.FLOYD_STEINBERG,
);

// Render result
const out = ctx.createImageData(dithered.width, dithered.height);
for (let i = 0; i < dithered.indices.length; i++) {
  const c = dithered.palette[dithered.indices[i]];
  out.data[i * 4] = c.r; out.data[i * 4 + 1] = c.g;
  out.data[i * 4 + 2] = c.b; out.data[i * 4 + 3] = 255;
}
ctx.putImageData(out, 0, 0);

Measured Palettes

Standard ColorScheme values use ideal sRGB colors (e.g. white = 255,255,255). Real e-paper displays reflect significantly less light. Use a measured ColorPalette for accurate dithering:

import { ditherImage, SPECTRA_7_3_6COLOR, BWRY_3_97 } from '@opendisplay/epaper-dithering';

// Automatically applies tone compression to fit the display's actual dynamic range
const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, DitherMode.BURKES);

Available measured palettes: SPECTRA_7_3_6COLOR, BWRY_3_97, MONO_4_26, BWRY_4_2, SOLUM_BWR, HANSHOW_BWR, HANSHOW_BWY.

Node.js (with sharp)

import sharp from 'sharp';
import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';

const { data, info } = await sharp('photo.jpg')
  .ensureAlpha()
  .raw()
  .toBuffer({ resolveWithObject: true });

const dithered = ditherImage(
  { width: info.width, height: info.height, data: new Uint8ClampedArray(data) },
  ColorScheme.BWR,
  DitherMode.BURKES,
);

const rgbaBuffer = Buffer.alloc(dithered.width * dithered.height * 4);
for (let i = 0; i < dithered.indices.length; i++) {
  const c = dithered.palette[dithered.indices[i]];
  rgbaBuffer[i * 4] = c.r; rgbaBuffer[i * 4 + 1] = c.g;
  rgbaBuffer[i * 4 + 2] = c.b; rgbaBuffer[i * 4 + 3] = 255;
}

await sharp(rgbaBuffer, { raw: { width: dithered.width, height: dithered.height, channels: 4 } })
  .png()
  .toFile('dithered.png');

API Reference

ditherImage(image, colorScheme, mode?, serpentine?)

Parameter Type Default Description
image ImageBuffer RGBA input image
colorScheme ColorScheme | ColorPalette Target palette (enum or measured)
mode DitherMode BURKES Dithering algorithm
serpentine boolean true Alternate row direction to reduce artifacts

Returns PaletteImageBuffer.

Color Schemes

enum ColorScheme {
  MONO         = 0,  // Black & White (2 colors)
  BWR          = 1,  // Black, White, Red (3 colors)
  BWY          = 2,  // Black, White, Yellow (3 colors)
  BWRY         = 3,  // Black, White, Red, Yellow (4 colors)
  BWGBRY       = 4,  // Black, White, Green, Blue, Red, Yellow (6 colors)
  GRAYSCALE_4  = 5,  // 4-level grayscale
  GRAYSCALE_8  = 6,  // 8-level grayscale
  GRAYSCALE_16 = 7,  // 16-level grayscale
}

Dither Modes

Mode Quality Speed Notes
NONE Fastest Direct palette mapping
ORDERED Low Very fast 4×4 Bayer matrix
SIERRA_LITE Medium Fast 3-neighbor kernel
FLOYD_STEINBERG Good Medium Most popular
BURKES Good Medium Default
ATKINSON Good Medium Classic Mac aesthetic
SIERRA High Medium
STUCKI Very high Slow
JARVIS_JUDICE_NINKE Highest Slowest

Types

interface ImageBuffer {
  width: number;
  height: number;
  data: Uint8ClampedArray; // RGBA, row-major
}

interface PaletteImageBuffer {
  width: number;
  height: number;
  indices: Uint8Array; // palette index per pixel
  palette: RGB[];      // sRGB colors
}

interface ColorPalette {
  readonly colors: Record<string, RGB>;
  readonly accent: string;
}

Preview Tool

An interactive browser tool for comparing dithering modes and palettes:

Hosted (always latest release): https://opendisplay.github.io/epaper-dithering/

Local (against your working branch):

cd packages/javascript
bun run dev
# → http://localhost:3456/dev.html

Features: drag & drop or paste from clipboard, live re-render on every setting change, timing display, palette swatch preview.

Related Projects