A Dart/Flutter package for applying dithering algorithms to images destined for E-Ink (electronic ink) displays. It quantizes a full-color image to the E-Ink palette while minimizing banding and contour artifacts. Typical use cases include E-Ink Displays, Thermal Printers, and LED / Dot-Matrix Screens.
Language: English | 中文
- 13 dithering kernels — error-diffusion (Floyd–Steinberg, Stucki, Atkinson, Jarvis–Judice–Ninke, Burkes, False Floyd–Steinberg (Heckbert), Sierra-3, Two-Row Sierra, Sierra Lite (Sierra-2-4A)), ordered (Bayer 2×2/4×4/8×8, Blue Noise), and none (no dithering).
- 4 scan orders for error-diffusion kernels (raster, serpentine, zigzag, Hilbert space-filling curve).
- 8 E-Ink palettes — from pure black/white up to 7-color (Gallery 7) and 16-level grayscale (Carta 16).
- Universal quantizer
EInkPaletteQuantizerthat maps any color to the nearest E-Ink color in a palette using Euclidean RGB distance. - A configurable
EInkImageProcessorwith both synchronous (process) and isolate-based asynchronous (processIsolated) processing.
Install via pub.dev → pub.dev/packages/eink_dither/install
import 'dart:typed_data';
import 'dart:io';
import 'package:eink_dither/eink_dither.dart';
Future<void> main() async {
final Uint8List bytes = await File('photo.jpg').readAsBytes();
// 1. Configure the processor.
final processor = EInkImageProcessor(
palette: EInkPalette.spectra6,
ditherKernel: DitherKernel.floydSteinberg,
scanOrder: DitherScanOrder.serpentine,
intensity: 1.0,
patternSize: 1,
maxSize: 700,
);
// 2a. Synchronous processing (blocks the current thread).
final image = processor.process(bytes);
// 2b. Asynchronous processing in a compute isolate (recommended for UI).
final image2 = await processor.processIsolated(bytes);
// 3. Encode the result (e.g. to PNG).
if (image2 != null) {
final png = img.encodePng(image2); // `image` package
await File('out.png').writeAsBytes(png);
}
}Enum of the ink colors an E-Ink display can render. Includes the CMY/RGB primaries, an
orange, and a 16-step gray ramp (gray1 … gray14, where step i renders as i * 17
in sRGB).
Enum of preset palettes:
| Palette | Inks | Colors |
|---|---|---|
bw |
2 | Black, White |
spectra3Red |
3 | Black, White, Red |
spectra3Yellow |
3 | Black, White, Yellow |
spectra4 |
4 | Black, White, Red, Yellow |
spectra3100Plus |
5 | Black, White, Red, Yellow, Orange |
spectra6 |
6 | Black, White, Red, Green, Blue, Yellow |
gallery7 |
7 | Black, White, Red, Yellow, Blue, Green, Orange |
carta16 |
16 | Black + 14 grays (i*17) + White |
A Quantizer (from the image package) that maps each pixel to the nearest color of a
palette by Euclidean RGB distance. Construct it directly:
final quantizer = EInkPaletteQuantizer([EInkColor.black, EInkColor.white]);
// or from a preset type:
final q2 = EInkPaletteQuantizer.of(EInkPalette.spectra6);process runs on the calling thread; processIsolated runs the
same work inside a compute isolate so the UI never blocks.
| Property | Type | Default | Description |
|---|---|---|---|
palette |
EInkPalette |
EInkPalette.spectra6 |
Target ink palette. |
ditherKernel |
DitherKernel |
DitherKernel.floydSteinberg |
Dithering algorithm. |
scanOrder |
DitherScanOrder |
DitherScanOrder.zigzag |
Pixel-visit order (error-diffusion only). |
intensity |
double |
1.0 |
Dither intensity for ordered kernels; ignored by error-diffusion. |
patternSize |
int |
1 |
Scales ordered-dither cells or error-diffusion blocks (larger = coarser). |
maxSize |
int |
800 |
Longest edge is capped to this (proportional resize). |
img.Image? process(Uint8List bytes);
Future<img.Image?> processIsolated(Uint8List bytes);The dithering algorithm. Error-diffusion kernels (none aside) propagate quantization
error to neighbours; ordered kernels (bayer2x2, bayer4x4, bayer8x8, blueNoise)
use a fixed threshold matrix and are position-independent.
enum DitherKernel {
none,
falseFloydSteinberg,
floydSteinberg,
stucki,
atkinson,
jarvisJudiceNinke,
burkes,
sierra3,
sierra2,
sierraLite,
bayer2x2,
bayer4x4,
bayer8x8,
blueNoise,
}The Sierra family (Frankie Sierra) trades quality for speed as the kernel shrinks:
| Kernel | Also known as | Neighbours | Divisor | Notes |
|---|---|---|---|---|
sierra3 |
Sierra, Sierra-3 | 10 | 32 | Three-row kernel, close to Jarvis quality but noticeably faster. |
sierra2 |
Two-Row Sierra | 7 | 16 | Two-row kernel, a good quality/speed compromise. |
sierraLite |
Sierra-2-4A | 3 | 4 | Smallest variant, fastest; slightly grainier than Floyd–Steinberg. |
The order in which pixels are visited by the error-diffusion kernels. It has no effect on the ordered (Bayer / blue-noise) kernels.
| Scan Order | Description |
|---|---|
raster |
Standard raster scan: every row traversed left to right, top to bottom. |
serpentine |
Boustrophedon (snake) scan: horizontal direction reverses every other row, reducing directional artifacts. |
zigzag |
Diagonal zigzag (JPEG-style) scan: visits pixels along anti-diagonals x + y == d, alternating each diagonal's direction, softening the horizontal worm patterns of raster scanning. |
hilbert |
Hilbert space-filling curve: consecutive pixels are adjacent on the grid, maximizing spatial locality; best reduction of directional artifacts among deterministic orders, approximating random-walk diffusion without losing determinism. |
All previews below were generated with EInkPalette.spectra6.
These kernels ignore scanOrder. intensity (threshold intensity) and patternSize
(threshold-cell scale) apply to them only.
none (no dithering) |
bayer2x2 |
bayer4x4 |
bayer8x8 |
blueNoise |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
The combination kernel × scanOrder produces a distinct texture.
If you need finer control, use ditherImage (or the ordered-only helpers
ditherImageBayer / ditherImageBlueNoise) directly with any Quantizer:
import 'package:image/image.dart' as img;
import 'package:eink_dither/eink_dither.dart';
final decoded = img.decodeImage(bytes)!;
final quantizer = EInkPaletteQuantizer.of(EInkPalette.spectra6);
final out = ditherImage(
decoded,
quantizer: quantizer,
kernel: DitherKernel.floydSteinberg,
scanOrder: DitherScanOrder.hilbert,
patternSize: 1,
);- Repository: github.com/runoob-coder/eink_dither
- Issue tracker: github.com/runoob-coder/eink_dither/issues
- Example app: The
example/directory contains a Flutter demo that lets you pick an image and tweak palette, kernel, scan order, intensity, and pattern size live. - Contributions: Pull requests and issues are welcome!
If eink_dither helps you build better UIs, please consider supporting it.
It only takes a few seconds and helps other Flutter developers discover the library.
This package stands on the shoulders of the researchers who pioneered digital dithering and halftoning. We gratefully acknowledge their foundational contributions:
- Floyd–Steinberg — Robert W. Floyd & Louis Steinberg (1975 - 1976), the classic error-diffusion kernel.
- False Floyd–Steinberg (Heckbert) — Paul Heckbert, introduced in his 1982 SIGGRAPH course notes Color Image Quantization for Frame Buffer Display.
- Jarvis–Judice–Ninke — J. F. Jarvis, C. N. Judice & W. H. Ninke (1976), Bell Labs.
- Stucki — Peter Stucki (1981), an optimized refinement of the Jarvis kernel at IBM.
- Burkes — Daniel Burkes, a simplified 7-pixel variant of the Jarvis–Judice–Ninke kernel.
- Atkinson — Bill Atkinson, created for MacPaint / HyperCard on early Macintosh systems.
- Sierra (Sierra-3), Two-Row Sierra (Sierra-2) and Sierra Lite (Sierra-2-4A) — Frankie Sierra (1989–1990), a family of progressively smaller kernels balancing quality against speed.
- Bayer (ordered dithering) — Bryce E. Bayer (1973), best known for the Bayer color filter array.
- Blue noise / void-and-cluster — Robert A. Ulichney (1987, 1993), Digital Halftoning, who formalized blue-noise dithering and the void-and-cluster mask generation method.









































