@opendisplay/epaper-dithering 2.1.3 → 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 +80 -126
- package/dist/index.cjs +408 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -16
- package/dist/index.d.ts +24 -16
- package/dist/index.js +402 -131
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# @opendisplay/epaper-dithering
|
|
2
2
|
|
|
3
|
+
[](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
|
-
- **
|
|
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**:
|
|
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
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
89
|
-
width: info.width,
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
101
|
-
rgbaBuffer[i * 4] =
|
|
102
|
-
rgbaBuffer[i * 4 +
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
113
|
+
Returns `PaletteImageBuffer`.
|
|
131
114
|
|
|
132
115
|
### Color Schemes
|
|
133
116
|
|
|
134
117
|
```typescript
|
|
135
118
|
enum ColorScheme {
|
|
136
|
-
MONO
|
|
137
|
-
BWR
|
|
138
|
-
BWY
|
|
139
|
-
BWRY
|
|
140
|
-
BWGBRY
|
|
141
|
-
GRAYSCALE_4
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
194
|
-
palette: RGB[];
|
|
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
|
-
##
|
|
204
|
-
|
|
205
|
-
### `getPalette(scheme: ColorScheme): ColorPalette`
|
|
166
|
+
## Local Development / Preview
|
|
206
167
|
|
|
207
|
-
|
|
168
|
+
A browser-based preview tool is included at [`dev.html`](./dev.html).
|
|
208
169
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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/)
|
|
231
|
-
- **OpenDisplay**: [`py-opendisplay`](https://github.com/OpenDisplay-org/py-opendisplay)
|
|
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
|