@opendisplay/epaper-dithering 2.1.4 → 2.2.1

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,19 @@
1
1
  # @opendisplay/epaper-dithering
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@opendisplay/epaper-dithering?style=flat-square)](https://www.npmjs.com/package/@opendisplay/epaper-dithering)
4
+
3
5
  High-quality dithering algorithms for e-paper/e-ink displays, implemented in TypeScript. Works in both browser and Node.js environments.
4
6
 
5
7
  ## Features
6
8
 
7
9
  - **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
10
+ - **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
13
+ - **Serpentine Scanning**: Alternates row direction to eliminate directional artifacts
9
14
  - **Universal**: Works in browser (Canvas API) and Node.js (with sharp/jimp)
10
15
  - **Zero Dependencies**: Pure TypeScript, no image library dependencies
11
- - **Fast**: Optimized typed array operations
12
- - **Type-Safe**: Full TypeScript support with exported types
16
+ - **Fast**: 256-entry sRGB LUT, pre-computed palette LAB arrays, typed array pixel buffers
13
17
 
14
18
  ## Installation
15
19
 
@@ -17,8 +21,6 @@ High-quality dithering algorithms for e-paper/e-ink displays, implemented in Typ
17
21
  npm install @opendisplay/epaper-dithering
18
22
  # or
19
23
  bun add @opendisplay/epaper-dithering
20
- # or
21
- yarn add @opendisplay/epaper-dithering
22
24
  ```
23
25
 
24
26
  ## Quick Start
@@ -28,12 +30,10 @@ yarn add @opendisplay/epaper-dithering
28
30
  ```typescript
29
31
  import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';
30
32
 
31
- // Load image
32
33
  const img = new Image();
33
34
  img.src = 'photo.jpg';
34
35
  await img.decode();
35
36
 
36
- // Convert to ImageBuffer
37
37
  const canvas = document.createElement('canvas');
38
38
  canvas.width = img.width;
39
39
  canvas.height = img.height;
@@ -41,157 +41,120 @@ const ctx = canvas.getContext('2d')!;
41
41
  ctx.drawImage(img, 0, 0);
42
42
  const imageData = ctx.getImageData(0, 0, img.width, img.height);
43
43
 
44
- const imageBuffer = {
45
- width: imageData.width,
46
- height: imageData.height,
47
- data: imageData.data,
48
- };
49
-
50
- // Dither
51
44
  const dithered = ditherImage(
52
- imageBuffer,
45
+ { width: imageData.width, height: imageData.height, data: imageData.data },
53
46
  ColorScheme.BWR,
54
- DitherMode.FLOYD_STEINBERG
47
+ DitherMode.FLOYD_STEINBERG,
55
48
  );
56
49
 
57
50
  // 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
-
51
+ const out = ctx.createImageData(dithered.width, dithered.height);
64
52
  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;
53
+ const c = dithered.palette[dithered.indices[i]];
54
+ out.data[i * 4] = c.r; out.data[i * 4 + 1] = c.g;
55
+ out.data[i * 4 + 2] = c.b; out.data[i * 4 + 3] = 255;
70
56
  }
57
+ ctx.putImageData(out, 0, 0);
58
+ ```
59
+
60
+ ### Measured Palettes
71
61
 
72
- resultCtx.putImageData(resultData, 0, 0);
73
- document.body.appendChild(resultCanvas);
62
+ 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:
63
+
64
+ ```typescript
65
+ import { ditherImage, SPECTRA_7_3_6COLOR, BWRY_3_97 } from '@opendisplay/epaper-dithering';
66
+
67
+ // Automatically applies tone compression to fit the display's actual dynamic range
68
+ const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, DitherMode.BURKES);
74
69
  ```
75
70
 
71
+ Available measured palettes: `SPECTRA_7_3_6COLOR`, `BWRY_3_97`, `MONO_4_26`, `BWRY_4_2`, `SOLUM_BWR`, `HANSHOW_BWR`, `HANSHOW_BWY`.
72
+
76
73
  ### Node.js (with sharp)
77
74
 
78
75
  ```typescript
79
76
  import sharp from 'sharp';
80
77
  import { ditherImage, ColorScheme, DitherMode } from '@opendisplay/epaper-dithering';
81
78
 
82
- // Load image
83
79
  const { data, info } = await sharp('photo.jpg')
84
80
  .ensureAlpha()
85
81
  .raw()
86
82
  .toBuffer({ resolveWithObject: true });
87
83
 
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);
84
+ const dithered = ditherImage(
85
+ { width: info.width, height: info.height, data: new Uint8ClampedArray(data) },
86
+ ColorScheme.BWR,
87
+ DitherMode.BURKES,
88
+ );
96
89
 
97
- // Convert back to RGBA
98
90
  const rgbaBuffer = Buffer.alloc(dithered.width * dithered.height * 4);
99
91
  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;
92
+ const c = dithered.palette[dithered.indices[i]];
93
+ rgbaBuffer[i * 4] = c.r; rgbaBuffer[i * 4 + 1] = c.g;
94
+ rgbaBuffer[i * 4 + 2] = c.b; rgbaBuffer[i * 4 + 3] = 255;
105
95
  }
106
96
 
107
- // Save
108
- await sharp(rgbaBuffer, {
109
- raw: {
110
- width: dithered.width,
111
- height: dithered.height,
112
- channels: 4,
113
- },
114
- })
97
+ await sharp(rgbaBuffer, { raw: { width: dithered.width, height: dithered.height, channels: 4 } })
115
98
  .png()
116
99
  .toFile('dithered.png');
117
100
  ```
118
101
 
119
102
  ## API Reference
120
103
 
121
- ### `ditherImage(image, colorScheme, mode?)`
122
-
123
- Apply dithering algorithm to image for e-paper display.
104
+ ### `ditherImage(image, colorScheme, mode?, serpentine?)`
124
105
 
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`)
106
+ | Parameter | Type | Default | Description |
107
+ |---|---|---|---|
108
+ | `image` | `ImageBuffer` | | RGBA input image |
109
+ | `colorScheme` | `ColorScheme \| ColorPalette` | — | Target palette (enum or measured) |
110
+ | `mode` | `DitherMode` | `BURKES` | Dithering algorithm |
111
+ | `serpentine` | `boolean` | `true` | Alternate row direction to reduce artifacts |
129
112
 
130
- **Returns:** `PaletteImageBuffer` - Palette-indexed image with color information
113
+ Returns `PaletteImageBuffer`.
131
114
 
132
115
  ### Color Schemes
133
116
 
134
117
  ```typescript
135
118
  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
119
+ MONO = 0, // Black & White (2 colors)
120
+ BWR = 1, // Black, White, Red (3 colors)
121
+ BWY = 2, // Black, White, Yellow (3 colors)
122
+ BWRY = 3, // Black, White, Red, Yellow (4 colors)
123
+ BWGBRY = 4, // Black, White, Green, Blue, Red, Yellow (6 colors)
124
+ GRAYSCALE_4 = 5, // 4-level grayscale
125
+ GRAYSCALE_8 = 6, // 8-level grayscale
126
+ GRAYSCALE_16 = 7, // 16-level grayscale
142
127
  }
143
128
  ```
144
129
 
145
130
  ### Dither Modes
146
131
 
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
132
+ | Mode | Quality | Speed | Notes |
133
+ |---|---|---|---|
134
+ | `NONE` | | Fastest | Direct palette mapping |
135
+ | `ORDERED` | Low | Very fast | 4×4 Bayer matrix |
136
+ | `SIERRA_LITE` | Medium | Fast | 3-neighbor kernel |
137
+ | `FLOYD_STEINBERG` | Good | Medium | Most popular |
138
+ | `BURKES` | Good | Medium | **Default** |
139
+ | `ATKINSON` | Good | Medium | Classic Mac aesthetic |
140
+ | `SIERRA` | High | Medium | — |
141
+ | `STUCKI` | Very high | Slow | — |
142
+ | `JARVIS_JUDICE_NINKE` | Highest | Slowest | — |
162
143
 
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 |
174
-
175
- ## Types
144
+ ### Types
176
145
 
177
146
  ```typescript
178
- interface RGB {
179
- r: number; // 0-255
180
- g: number; // 0-255
181
- b: number; // 0-255
182
- }
183
-
184
147
  interface ImageBuffer {
185
148
  width: number;
186
149
  height: number;
187
- data: Uint8ClampedArray; // RGBA format
150
+ data: Uint8ClampedArray; // RGBA, row-major
188
151
  }
189
152
 
190
153
  interface PaletteImageBuffer {
191
154
  width: number;
192
155
  height: number;
193
- indices: Uint8Array; // Palette index per pixel
194
- palette: RGB[]; // Available colors
156
+ indices: Uint8Array; // palette index per pixel
157
+ palette: RGB[]; // sRGB colors
195
158
  }
196
159
 
197
160
  interface ColorPalette {
@@ -200,32 +163,23 @@ interface ColorPalette {
200
163
  }
201
164
  ```
202
165
 
203
- ## Helper Functions
204
-
205
- ### `getPalette(scheme: ColorScheme): ColorPalette`
166
+ ## Local Development / Preview
206
167
 
207
- Get color palette for a color scheme.
168
+ A browser-based preview tool is included at [`dev.html`](./dev.html).
208
169
 
209
- ### `getColorCount(scheme: ColorScheme): number`
210
-
211
- Get number of colors in a color scheme.
212
-
213
- ### `fromValue(value: number): ColorScheme`
214
-
215
- Create ColorScheme from firmware integer value (0-5).
216
-
217
- ## Performance
218
-
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
170
+ ```bash
171
+ cd packages/javascript
172
+ bun run dev
173
+ # opens http://localhost:3456/dev.html
174
+ ```
225
175
 
226
- Performance varies by device and JavaScript engine.
176
+ Features:
177
+ - drag & drop or paste from clipboard
178
+ - live re-render on setting change
179
+ - timing display
180
+ - palette swatch preview.
227
181
 
228
182
  ## Related Projects
229
183
 
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
184
+ - **Python**: [`epaper-dithering`](https://pypi.org/project/epaper-dithering/) Python implementation (feature superset)
185
+ - **OpenDisplay**: [`py-opendisplay`](https://github.com/OpenDisplay-org/py-opendisplay) Python library for OpenDisplay BLE devices