Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ img = Image.open("photo.jpg")
# Idealized palette
dithered = dither_image(img, ColorScheme.BWR, mode=DitherMode.FLOYD_STEINBERG)

# Measured palette — auto tone + gamut compression
# Measured palette — calibrated color matching
dithered = dither_image(img, SPECTRA_7_3_6COLOR_V2)

# Opt in to automatic tone + gamut compression for photos
dithered = dither_image(img, SPECTRA_7_3_6COLOR_V2, tone="auto", gamut="auto")
```

See [`packages/python/README.md`](packages/python/README.md) for full documentation.
Expand All @@ -59,8 +62,11 @@ import { ditherImage, ColorScheme, DitherMode, SPECTRA_7_3_6COLOR_V2 } from '@op
// ImageBuffer from Canvas API or Node.js (sharp, etc.)
const dithered = ditherImage(imageBuffer, ColorScheme.BWR, { mode: DitherMode.BURKES });

// Measured palette — auto tone + gamut compression
// Measured palette — calibrated color matching
const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR_V2);

// Opt in to automatic tone + gamut compression for photos
const compressed = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR_V2, { tone: 'auto', gamut: 'auto' });
```

See [`packages/javascript/README.md`](packages/javascript/README.md) for full documentation.
Expand All @@ -70,7 +76,7 @@ See [`packages/javascript/README.md`](packages/javascript/README.md) for full do
- **Rust Core**: All dithering logic in `packages/rust/core/` — shared by both packages
- **9 Dithering Algorithms**: NONE, ORDERED, BURKES, FLOYD_STEINBERG, ATKINSON, STUCKI, SIERRA, SIERRA_LITE, JARVIS_JUDICE_NINKE
- **8 Color Schemes**: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE_4, GRAYSCALE_8, GRAYSCALE_16
- **Measured Palettes**: Calibrated RGB values for real displays with tone + gamut compression
- **Measured Palettes**: Calibrated RGB values for real displays, linked to their canonical firmware palette
- **OKLab Color Matching**: Weighted Cartesian OKLab — preserves hue without the achromatic-attractor bug of LCH-weighted approaches
- **Pre-dither Knobs**: Per-image exposure, saturation, shadows, highlights, and gamut compression — all orthogonal

Expand Down
17 changes: 14 additions & 3 deletions packages/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ Standard `ColorScheme` values use ideal sRGB colors (e.g. white = 255,255,255).
```typescript
import { ditherImage, SPECTRA_7_3_6COLOR, BWRY_3_97 } from '@opendisplay/epaper-dithering';

// Automatically applies tone compression to fit the display's actual dynamic range
const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, { mode: DitherMode.BURKES });

// Opt in when you want automatic tone/gamut compression for photos
const compressed = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR, {
mode: DitherMode.BURKES,
tone: 'auto',
gamut: 'auto',
});
```

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`.
Expand Down Expand Up @@ -116,11 +122,15 @@ ditherImage(image: ImageBuffer, palette: ColorScheme | ColorPalette, options?: D
| `saturation` | `number` | `1.0` | OKLab saturation multiplier. `0.0` = grayscale, `>1` = boost. Hue-preserving |
| `shadows` | `number` | `0.0` | Shadow lift strength (S-curve lower half). `0.0` = off, `1.0` = strong |
| `highlights` | `number` | `0.0` | Highlight compression strength (S-curve upper half). `0.0` = off, `1.0` = strong |
| `tone` | `number \| 'auto' \| 'off'` | `'auto'` | Dynamic range compression. `'auto'` = histogram-based; numeric = fixed strength. Ignored for `ColorScheme` |
| `gamut` | `number \| 'auto' \| 'off'` | `'auto'` | Pre-dither gamut compression. `'auto'` = activate when image exceeds palette gamut; numeric = fixed. Ignored for `ColorScheme` |
| `tone` | `number \| 'auto' \| 'off'` | `0.0` | Dynamic range compression. `0.0`/`'off'` = disabled; `'auto'` = histogram-based; numeric = fixed strength. Ignored for `ColorScheme` |
| `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` |

Pre-processing pipeline: `exposure → saturation → shadows/highlights → tone → gamut → dither`. Each step is a no-op at its identity value.

`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.

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.

Returns `PaletteImageBuffer`.

### Color Schemes
Expand Down Expand Up @@ -171,6 +181,7 @@ interface PaletteImageBuffer {
interface ColorPalette {
readonly colors: Record<string, RGB>;
readonly accent: string;
readonly scheme?: number;
}
```

Expand Down
Loading
Loading