@opendisplay/epaper-dithering 2.2.1 → 4.1.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
@@ -2,18 +2,19 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@opendisplay/epaper-dithering?style=flat-square)](https://www.npmjs.com/package/@opendisplay/epaper-dithering)
4
4
 
5
- High-quality dithering algorithms for e-paper/e-ink displays, implemented in TypeScript. Works in both browser and Node.js environments.
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.
6
6
 
7
7
  ## Features
8
8
 
9
+ - **Rust/WASM Core**: Compiled Rust logic bundled inline — no async init, no external files, works everywhere
9
10
  - **9 Dithering Algorithms**: From fast ordered dithering to high-quality error diffusion
10
11
  - **8 Color Schemes**: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE\_4/8/16
11
- - **Measured Palettes**: Use real display-calibrated colors for accurate dithering (SPECTRA\_7\_3\_6COLOR, BWRY\_3\_97, and more)
12
- - **LCH Color Matching**: Perceptual LAB color space with hue-weighted distance hue errors can't be recovered by error diffusion, so they're prioritized
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
13
15
  - **Serpentine Scanning**: Alternates row direction to eliminate directional artifacts
14
- - **Universal**: Works in browser (Canvas API) and Node.js (with sharp/jimp)
15
- - **Zero Dependencies**: Pure TypeScript, no image library dependencies
16
- - **Fast**: 256-entry sRGB LUT, pre-computed palette LAB arrays, typed array pixel buffers
16
+ - **Universal**: Works in browser (Canvas API) and Node.js (≥18)
17
+ - **Zero Dependencies**: WASM binary bundled inline, no image library required
17
18
 
18
19
  ## Installation
19
20
 
@@ -44,7 +45,7 @@ const imageData = ctx.getImageData(0, 0, img.width, img.height);
44
45
  const dithered = ditherImage(
45
46
  { width: imageData.width, height: imageData.height, data: imageData.data },
46
47
  ColorScheme.BWR,
47
- DitherMode.FLOYD_STEINBERG,
48
+ { mode: DitherMode.FLOYD_STEINBERG },
48
49
  );
49
50
 
50
51
  // Render result
@@ -64,11 +65,17 @@ Standard `ColorScheme` values use ideal sRGB colors (e.g. white = 255,255,255).
64
65
  ```typescript
65
66
  import { ditherImage, SPECTRA_7_3_6COLOR, BWRY_3_97 } from '@opendisplay/epaper-dithering';
66
67
 
67
- // Automatically applies tone compression to fit the display's actual dynamic range
68
- const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, DitherMode.BURKES);
68
+ const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, { mode: DitherMode.BURKES });
69
+
70
+ // Opt in when you want automatic tone/gamut compression for photos
71
+ const compressed = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, {
72
+ mode: DitherMode.BURKES,
73
+ tone: 'auto',
74
+ gamut: 'auto',
75
+ });
69
76
  ```
70
77
 
71
- Available measured palettes: `SPECTRA_7_3_6COLOR`, `BWRY_3_97`, `MONO_4_26`, `BWRY_4_2`, `SOLUM_BWR`, `HANSHOW_BWR`, `HANSHOW_BWY`.
78
+ 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`.
72
79
 
73
80
  ### Node.js (with sharp)
74
81
 
@@ -84,7 +91,7 @@ const { data, info } = await sharp('photo.jpg')
84
91
  const dithered = ditherImage(
85
92
  { width: info.width, height: info.height, data: new Uint8ClampedArray(data) },
86
93
  ColorScheme.BWR,
87
- DitherMode.BURKES,
94
+ { mode: DitherMode.BURKES },
88
95
  );
89
96
 
90
97
  const rgbaBuffer = Buffer.alloc(dithered.width * dithered.height * 4);
@@ -101,14 +108,28 @@ await sharp(rgbaBuffer, { raw: { width: dithered.width, height: dithered.height,
101
108
 
102
109
  ## API Reference
103
110
 
104
- ### `ditherImage(image, colorScheme, mode?, serpentine?)`
111
+ ### `ditherImage(image, colorScheme, options?)`
112
+
113
+ ```typescript
114
+ ditherImage(image: ImageBuffer, palette: ColorScheme | ColorPalette, options?: DitherOptions): PaletteImageBuffer
115
+ ```
105
116
 
106
- | Parameter | Type | Default | Description |
117
+ | `options` field | Type | Default | Description |
107
118
  |---|---|---|---|
108
- | `image` | `ImageBuffer` | — | RGBA input image |
109
- | `colorScheme` | `ColorScheme \| ColorPalette` | — | Target palette (enum or measured) |
110
119
  | `mode` | `DitherMode` | `BURKES` | Dithering algorithm |
111
120
  | `serpentine` | `boolean` | `true` | Alternate row direction to reduce artifacts |
121
+ | `exposure` | `number` | `1.0` | Linear-RGB exposure multiplier. `2.0` = +1 stop, `0.5` = −1 stop |
122
+ | `saturation` | `number` | `1.0` | OKLab saturation multiplier. `0.0` = grayscale, `>1` = boost. Hue-preserving |
123
+ | `shadows` | `number` | `0.0` | Shadow lift strength (S-curve lower half). `0.0` = off, `1.0` = strong |
124
+ | `highlights` | `number` | `0.0` | Highlight compression strength (S-curve upper half). `0.0` = off, `1.0` = strong |
125
+ | `tone` | `number \| 'auto' \| 'off'` | `0.0` | Dynamic range compression. `0.0`/`'off'` = disabled; `'auto'` = histogram-based; numeric = fixed strength. Ignored for `ColorScheme` |
126
+ | `gamut` | `number \| 'auto' \| 'off'` | `0.0` | Pre-dither gamut compression. `0.0`/`'off'` = disabled; `'auto'` = activate when image exceeds palette gamut; numeric = fixed. Ignored for `ColorScheme` |
127
+
128
+ Pre-processing pipeline: `exposure → saturation → shadows/highlights → tone → gamut → dither`. Each step is a no-op at its identity value.
129
+
130
+ `DitherMode.NONE` performs direct nearest-color mapping without error diffusion or ordered dithering. Built-in measured palettes carry their canonical firmware `scheme`, so pure display colors map to the corresponding firmware palette index even when measured RGB values are used for matching.
131
+
132
+ For built-in measured palettes, exact canonical display colors are also protected in ordered and error-diffusion modes when pre-processing is off: an image made entirely of display colors is returned as a direct palette-index map, and exact display-color pixels inside a mixed image keep their canonical index instead of being rematched to the measured RGB palette. Pre-processing runs before that exact-pixel check, so explicit `tone: 'auto'`, `gamut: 'auto'`, or other adjustments may intentionally alter those pixels first.
112
133
 
113
134
  Returns `PaletteImageBuffer`.
114
135
 
@@ -160,26 +181,39 @@ interface PaletteImageBuffer {
160
181
  interface ColorPalette {
161
182
  readonly colors: Record<string, RGB>;
162
183
  readonly accent: string;
184
+ readonly scheme?: number;
163
185
  }
164
186
  ```
165
187
 
166
- ## Local Development / Preview
188
+ ## Preview Tool
167
189
 
168
- A browser-based preview tool is included at [`dev.html`](./dev.html).
190
+ An interactive browser tool for comparing dithering modes and palettes:
169
191
 
192
+ **Hosted** (always latest release): https://opendisplay.github.io/epaper-dithering/
193
+
194
+ **Local** (against your working branch):
170
195
  ```bash
171
196
  cd packages/javascript
172
197
  bun run dev
173
- # opens http://localhost:3456/dev.html
198
+ # http://localhost:3456/dev.html
174
199
  ```
175
200
 
176
- Features:
177
- - drag & drop or paste from clipboard
178
- - live re-render on setting change
179
- - timing display
180
- - palette swatch preview.
201
+ Features: drag & drop or paste from clipboard, live re-render on every setting change, timing display, palette swatch preview.
202
+
203
+ ## Development
204
+
205
+ ```bash
206
+ bun install
207
+
208
+ # When Rust source changes, rebuild the WASM (from repo root):
209
+ wasm-pack build packages/rust/wasm --target bundler --out-dir ../../javascript/src/wasm-core
210
+
211
+ bun run test # vitest
212
+ bun run build # tsup → dist/
213
+ bun run type-check
214
+ ```
181
215
 
182
216
  ## Related Projects
183
217
 
184
- - **Python**: [`epaper-dithering`](https://pypi.org/project/epaper-dithering/) — Python implementation (feature superset)
218
+ - **Python**: [`epaper-dithering`](https://pypi.org/project/epaper-dithering/) — Python package, shares the same Rust core
185
219
  - **OpenDisplay**: [`py-opendisplay`](https://github.com/OpenDisplay-org/py-opendisplay) — Python library for OpenDisplay BLE devices