@opendisplay/epaper-dithering 2.1.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,20 @@
1
1
  # @opendisplay/epaper-dithering
2
2
 
3
- High-quality dithering algorithms for e-paper/e-ink displays, implemented in TypeScript. Works in both browser and Node.js environments.
3
+ [![npm](https://img.shields.io/npm/v/@opendisplay/epaper-dithering?style=flat-square)](https://www.npmjs.com/package/@opendisplay/epaper-dithering)
4
+
5
+ High-quality dithering algorithms for e-paper/e-ink displays, powered by a Rust/WASM core. Works in both browser and Node.js environments.
4
6
 
5
7
  ## Features
6
8
 
9
+ - **Rust/WASM Core**: Compiled Rust logic bundled inline — no async init, no external files, works everywhere
7
10
  - **9 Dithering Algorithms**: From fast ordered dithering to high-quality error diffusion
8
- - **6 Color Schemes**: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE_4
9
- - **Universal**: Works in browser (Canvas API) and Node.js (with sharp/jimp)
10
- - **Zero Dependencies**: Pure TypeScript, no image library dependencies
11
- - **Fast**: Optimized typed array operations
12
- - **Type-Safe**: Full TypeScript support with exported types
11
+ - **8 Color Schemes**: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE\_4/8/16
12
+ - **Measured Palettes**: Use real display-calibrated colors for accurate dithering (SPECTRA\_7\_3\_6COLOR\_V2, BWRY\_3\_97, and more)
13
+ - **OKLab Color Matching**: Weighted Cartesian OKLab preserves hue without the achromatic-attractor bug that plagues LCH-weighted approaches
14
+ - **Pre-dither Adjustments**: Per-image exposure, saturation, shadows, highlights, dynamic-range compression, and gamut compression — all orthogonal knobs
15
+ - **Serpentine Scanning**: Alternates row direction to eliminate directional artifacts
16
+ - **Universal**: Works in browser (Canvas API) and Node.js (≥18)
17
+ - **Zero Dependencies**: WASM binary bundled inline, no image library required
13
18
 
14
19
  ## Installation
15
20
 
@@ -17,8 +22,6 @@ High-quality dithering algorithms for e-paper/e-ink displays, implemented in Typ
17
22
  npm install @opendisplay/epaper-dithering
18
23
  # or
19
24
  bun add @opendisplay/epaper-dithering
20
- # or
21
- yarn add @opendisplay/epaper-dithering
22
25
  ```
23
26
 
24
27
  ## Quick Start
@@ -28,12 +31,10 @@ yarn add @opendisplay/epaper-dithering
28
31
  ```typescript
29
32
  import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';
30
33
 
31
- // Load image
32
34
  const img = new Image();
33
35
  img.src = 'photo.jpg';
34
36
  await img.decode();
35
37
 
36
- // Convert to ImageBuffer
37
38
  const canvas = document.createElement('canvas');
38
39
  canvas.width = img.width;
39
40
  canvas.height = img.height;
@@ -41,157 +42,130 @@ const ctx = canvas.getContext('2d')!;
41
42
  ctx.drawImage(img, 0, 0);
42
43
  const imageData = ctx.getImageData(0, 0, img.width, img.height);
43
44
 
44
- const imageBuffer = {
45
- width: imageData.width,
46
- height: imageData.height,
47
- data: imageData.data,
48
- };
49
-
50
- // Dither
51
45
  const dithered = ditherImage(
52
- imageBuffer,
46
+ { width: imageData.width, height: imageData.height, data: imageData.data },
53
47
  ColorScheme.BWR,
54
- DitherMode.FLOYD_STEINBERG
48
+ { mode: DitherMode.FLOYD_STEINBERG },
55
49
  );
56
50
 
57
51
  // Render result
58
- const resultCanvas = document.createElement('canvas');
59
- resultCanvas.width = dithered.width;
60
- resultCanvas.height = dithered.height;
61
- const resultCtx = resultCanvas.getContext('2d')!;
62
- const resultData = resultCtx.createImageData(dithered.width, dithered.height);
63
-
52
+ const out = ctx.createImageData(dithered.width, dithered.height);
64
53
  for (let i = 0; i < dithered.indices.length; i++) {
65
- const color = dithered.palette[dithered.indices[i]];
66
- resultData.data[i * 4] = color.r;
67
- resultData.data[i * 4 + 1] = color.g;
68
- resultData.data[i * 4 + 2] = color.b;
69
- resultData.data[i * 4 + 3] = 255;
54
+ const c = dithered.palette[dithered.indices[i]];
55
+ out.data[i * 4] = c.r; out.data[i * 4 + 1] = c.g;
56
+ out.data[i * 4 + 2] = c.b; out.data[i * 4 + 3] = 255;
70
57
  }
58
+ ctx.putImageData(out, 0, 0);
59
+ ```
60
+
61
+ ### Measured Palettes
71
62
 
72
- resultCtx.putImageData(resultData, 0, 0);
73
- document.body.appendChild(resultCanvas);
63
+ 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:
64
+
65
+ ```typescript
66
+ import { ditherImage, SPECTRA_7_3_6COLOR, BWRY_3_97 } from '@opendisplay/epaper-dithering';
67
+
68
+ // Automatically applies tone compression to fit the display's actual dynamic range
69
+ const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, { mode: DitherMode.BURKES });
74
70
  ```
75
71
 
72
+ Available measured palettes: `SPECTRA_7_3_6COLOR_V2`, `SPECTRA_7_3_6COLOR`, `BWRY_3_97`, `MONO_4_26`, `BWRY_4_2`, `SOLUM_BWR`, `HANSHOW_BWR`, `HANSHOW_BWY`.
73
+
76
74
  ### Node.js (with sharp)
77
75
 
78
76
  ```typescript
79
77
  import sharp from 'sharp';
80
78
  import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';
81
79
 
82
- // Load image
83
80
  const { data, info } = await sharp('photo.jpg')
84
81
  .ensureAlpha()
85
82
  .raw()
86
83
  .toBuffer({ resolveWithObject: true });
87
84
 
88
- const imageBuffer = {
89
- width: info.width,
90
- height: info.height,
91
- data: new Uint8ClampedArray(data),
92
- };
93
-
94
- // Dither
95
- const dithered = ditherImage(imageBuffer, ColorScheme.BWR, DitherMode.BURKES);
85
+ const dithered = ditherImage(
86
+ { width: info.width, height: info.height, data: new Uint8ClampedArray(data) },
87
+ ColorScheme.BWR,
88
+ { mode: DitherMode.BURKES },
89
+ );
96
90
 
97
- // Convert back to RGBA
98
91
  const rgbaBuffer = Buffer.alloc(dithered.width * dithered.height * 4);
99
92
  for (let i = 0; i < dithered.indices.length; i++) {
100
- const color = dithered.palette[dithered.indices[i]];
101
- rgbaBuffer[i * 4] = color.r;
102
- rgbaBuffer[i * 4 + 1] = color.g;
103
- rgbaBuffer[i * 4 + 2] = color.b;
104
- rgbaBuffer[i * 4 + 3] = 255;
93
+ const c = dithered.palette[dithered.indices[i]];
94
+ rgbaBuffer[i * 4] = c.r; rgbaBuffer[i * 4 + 1] = c.g;
95
+ rgbaBuffer[i * 4 + 2] = c.b; rgbaBuffer[i * 4 + 3] = 255;
105
96
  }
106
97
 
107
- // Save
108
- await sharp(rgbaBuffer, {
109
- raw: {
110
- width: dithered.width,
111
- height: dithered.height,
112
- channels: 4,
113
- },
114
- })
98
+ await sharp(rgbaBuffer, { raw: { width: dithered.width, height: dithered.height, channels: 4 } })
115
99
  .png()
116
100
  .toFile('dithered.png');
117
101
  ```
118
102
 
119
103
  ## API Reference
120
104
 
121
- ### `ditherImage(image, colorScheme, mode?)`
105
+ ### `ditherImage(image, colorScheme, options?)`
122
106
 
123
- Apply dithering algorithm to image for e-paper display.
107
+ ```typescript
108
+ ditherImage(image: ImageBuffer, palette: ColorScheme | ColorPalette, options?: DitherOptions): PaletteImageBuffer
109
+ ```
110
+
111
+ | `options` field | Type | Default | Description |
112
+ |---|---|---|---|
113
+ | `mode` | `DitherMode` | `BURKES` | Dithering algorithm |
114
+ | `serpentine` | `boolean` | `true` | Alternate row direction to reduce artifacts |
115
+ | `exposure` | `number` | `1.0` | Linear-RGB exposure multiplier. `2.0` = +1 stop, `0.5` = −1 stop |
116
+ | `saturation` | `number` | `1.0` | OKLab saturation multiplier. `0.0` = grayscale, `>1` = boost. Hue-preserving |
117
+ | `shadows` | `number` | `0.0` | Shadow lift strength (S-curve lower half). `0.0` = off, `1.0` = strong |
118
+ | `highlights` | `number` | `0.0` | Highlight compression strength (S-curve upper half). `0.0` = off, `1.0` = strong |
119
+ | `tone` | `number \| 'auto' \| 'off'` | `'auto'` | Dynamic range compression. `'auto'` = histogram-based; numeric = fixed strength. Ignored for `ColorScheme` |
120
+ | `gamut` | `number \| 'auto' \| 'off'` | `'auto'` | Pre-dither gamut compression. `'auto'` = activate when image exceeds palette gamut; numeric = fixed. Ignored for `ColorScheme` |
124
121
 
125
- **Parameters:**
126
- - `image: ImageBuffer` - Input image in RGBA format
127
- - `colorScheme: ColorScheme` - Target e-paper color scheme
128
- - `mode?: DitherMode` - Dithering algorithm (default: `DitherMode.BURKES`)
122
+ Pre-processing pipeline: `exposure → saturation → shadows/highlights → tone → gamut → dither`. Each step is a no-op at its identity value.
129
123
 
130
- **Returns:** `PaletteImageBuffer` - Palette-indexed image with color information
124
+ Returns `PaletteImageBuffer`.
131
125
 
132
126
  ### Color Schemes
133
127
 
134
128
  ```typescript
135
129
  enum ColorScheme {
136
- MONO = 0, // Black & White (2 colors)
137
- BWR = 1, // Black, White, Red (3 colors)
138
- BWY = 2, // Black, White, Yellow (3 colors)
139
- BWRY = 3, // Black, White, Red, Yellow (4 colors)
140
- BWGBRY = 4, // Black, White, Green, Blue, Red, Yellow (6 colors)
141
- GRAYSCALE_4 = 5, // 4-level grayscale
130
+ MONO = 0, // Black & White (2 colors)
131
+ BWR = 1, // Black, White, Red (3 colors)
132
+ BWY = 2, // Black, White, Yellow (3 colors)
133
+ BWRY = 3, // Black, White, Red, Yellow (4 colors)
134
+ BWGBRY = 4, // Black, White, Green, Blue, Red, Yellow (6 colors)
135
+ GRAYSCALE_4 = 5, // 4-level grayscale
136
+ GRAYSCALE_8 = 6, // 8-level grayscale
137
+ GRAYSCALE_16 = 7, // 16-level grayscale
142
138
  }
143
139
  ```
144
140
 
145
141
  ### Dither Modes
146
142
 
147
- ```typescript
148
- enum DitherMode {
149
- NONE = 0, // No dithering (direct palette mapping)
150
- BURKES = 1, // Burkes (default - good quality/speed balance)
151
- ORDERED = 2, // Ordered (4×4 Bayer matrix - fast)
152
- FLOYD_STEINBERG = 3, // Floyd-Steinberg (most popular)
153
- ATKINSON = 4, // Atkinson (classic Macintosh style)
154
- STUCKI = 5, // Stucki (high quality)
155
- SIERRA = 6, // Sierra (high quality)
156
- SIERRA_LITE = 7, // Sierra Lite (fast)
157
- JARVIS_JUDICE_NINKE = 8, // Jarvis-Judice-Ninke (highest quality, slowest)
158
- }
159
- ```
160
-
161
- ## Algorithm Comparison
162
-
163
- | Algorithm | Quality | Speed | Use Case |
164
- |-----------|---------|-------|----------|
165
- | NONE | Lowest | Fastest | Testing, solid colors |
166
- | ORDERED | Low | Very Fast | Simple images, patterns |
167
- | SIERRA_LITE | Medium | Fast | Quick previews |
168
- | BURKES | Good | Medium | **Default - best balance** |
169
- | FLOYD_STEINBERG | Good | Medium | Popular choice, smooth gradients |
170
- | ATKINSON | Good | Medium | Classic retro aesthetic |
171
- | SIERRA | High | Medium | Detailed images |
172
- | STUCKI | Very High | Slow | Photos, high detail |
173
- | JARVIS_JUDICE_NINKE | Highest | Slowest | Maximum quality |
143
+ | Mode | Quality | Speed | Notes |
144
+ |---|---|---|---|
145
+ | `NONE` | | Fastest | Direct palette mapping |
146
+ | `ORDERED` | Low | Very fast | 4×4 Bayer matrix |
147
+ | `SIERRA_LITE` | Medium | Fast | 3-neighbor kernel |
148
+ | `FLOYD_STEINBERG` | Good | Medium | Most popular |
149
+ | `BURKES` | Good | Medium | **Default** |
150
+ | `ATKINSON` | Good | Medium | Classic Mac aesthetic |
151
+ | `SIERRA` | High | Medium | — |
152
+ | `STUCKI` | Very high | Slow | — |
153
+ | `JARVIS_JUDICE_NINKE` | Highest | Slowest | — |
174
154
 
175
- ## Types
155
+ ### Types
176
156
 
177
157
  ```typescript
178
- interface RGB {
179
- r: number; // 0-255
180
- g: number; // 0-255
181
- b: number; // 0-255
182
- }
183
-
184
158
  interface ImageBuffer {
185
159
  width: number;
186
160
  height: number;
187
- data: Uint8ClampedArray; // RGBA format
161
+ data: Uint8ClampedArray; // RGBA, row-major
188
162
  }
189
163
 
190
164
  interface PaletteImageBuffer {
191
165
  width: number;
192
166
  height: number;
193
- indices: Uint8Array; // Palette index per pixel
194
- palette: RGB[]; // Available colors
167
+ indices: Uint8Array; // palette index per pixel
168
+ palette: RGB[]; // sRGB colors
195
169
  }
196
170
 
197
171
  interface ColorPalette {
@@ -200,32 +174,35 @@ interface ColorPalette {
200
174
  }
201
175
  ```
202
176
 
203
- ## Helper Functions
177
+ ## Preview Tool
204
178
 
205
- ### `getPalette(scheme: ColorScheme): ColorPalette`
179
+ An interactive browser tool for comparing dithering modes and palettes:
206
180
 
207
- Get color palette for a color scheme.
181
+ **Hosted** (always latest release): https://opendisplay.github.io/epaper-dithering/
208
182
 
209
- ### `getColorCount(scheme: ColorScheme): number`
210
-
211
- Get number of colors in a color scheme.
183
+ **Local** (against your working branch):
184
+ ```bash
185
+ cd packages/javascript
186
+ bun run dev
187
+ # → http://localhost:3456/dev.html
188
+ ```
212
189
 
213
- ### `fromValue(value: number): ColorScheme`
190
+ Features: drag & drop or paste from clipboard, live re-render on every setting change, timing display, palette swatch preview.
214
191
 
215
- Create ColorScheme from firmware integer value (0-5).
192
+ ## Development
216
193
 
217
- ## Performance
194
+ ```bash
195
+ bun install
218
196
 
219
- Expected performance on an 800×600 image:
220
- - **ORDERED**: ~50ms
221
- - **SIERRA_LITE**: ~100ms
222
- - **BURKES/FLOYD_STEINBERG**: ~150ms
223
- - **SIERRA/ATKINSON**: ~200ms
224
- - **STUCKI/JARVIS**: ~300ms
197
+ # When Rust source changes, rebuild the WASM (from repo root):
198
+ wasm-pack build packages/rust/wasm --target bundler --out-dir ../../javascript/src/wasm-core
225
199
 
226
- Performance varies by device and JavaScript engine.
200
+ bun run test # vitest
201
+ bun run build # tsup → dist/
202
+ bun run type-check
203
+ ```
227
204
 
228
205
  ## Related Projects
229
206
 
230
- - **Python**: [`epaper-dithering`](https://pypi.org/project/epaper-dithering/) - Python implementation
231
- - **OpenDisplay**: [`py-opendisplay`](https://github.com/OpenDisplay-org/py-opendisplay) - Python library for OpenDisplay BLE Devices
207
+ - **Python**: [`epaper-dithering`](https://pypi.org/project/epaper-dithering/) Python package, shares the same Rust core
208
+ - **OpenDisplay**: [`py-opendisplay`](https://github.com/OpenDisplay-org/py-opendisplay) Python library for OpenDisplay BLE devices