Dither Images for E-Paper Displays in JavaScript – EPD Optimize
It allows you to build browser-based and Node.js tools that reduce, tone-map, dither, preview, and export images for limited-color e-paper hardware.
The library works well on JavaScript e-paper image dithering workflows for Spectra 6, AcEP or Gallery displays, black-and-white displays, retro palettes, and custom device palettes.
Install EPD Optimize with NPM.
npm install epdoptimize
Draw the source image into a canvas before calling the library. The first output canvas holds the calibrated preview, and the second output canvas holds the device-color version.
<canvas id="source-photo"></canvas> <canvas id="calibrated-preview"></canvas> <canvas id="device-output"></canvas>
import {
ditherImage,
replaceColors,
aitjcizeSpectra6Palette,
} from "epdoptimize";
const sourceCanvas = document.querySelector("#source-photo");
const calibratedCanvas = document.querySelector("#calibrated-preview");
const deviceCanvas = document.querySelector("#device-output");
// Convert the source canvas into calibrated Spectra 6 colors.
await ditherImage(sourceCanvas, calibratedCanvas, {
palette: aitjcizeSpectra6Palette,
processingPreset: "balanced",
ditheringType: "errorDiffusion",
errorDiffusionMatrix: "floydSteinberg",
serpentine: true,
});
// Convert calibrated colors into native device colors for export.
replaceColors(calibratedCanvas, deviceCanvas, aitjcizeSpectra6Palette);
Photo uploads often need automatic settings because source images vary in contrast, color range, and detail. The recommender analyzes the image and returns concrete dither options for the selected palette.
import {
ditherImage,
replaceColors,
aitjcizeSpectra6Palette,
suggestCanvasProcessingOptions,
} from "epdoptimize";
const suggestion = suggestCanvasProcessingOptions(
sourceCanvas,
aitjcizeSpectra6Palette,
{
intent: "natural",
},
);
// Apply the recommended options to the selected e-paper palette.
await ditherImage(sourceCanvas, calibratedCanvas, {
...suggestion.ditherOptions,
palette: aitjcizeSpectra6Palette,
});
// Export the native device colors from the calibrated preview.
replaceColors(calibratedCanvas, deviceCanvas, aitjcizeSpectra6Palette);
console.log(suggestion.imageKind);
console.log(suggestion.reasons);
A readable poster or label image needs stronger separation between text and background. The readable intent steers the recommender toward processing settings that favor clarity.
const readableSuggestion = suggestCanvasProcessingOptions(
sourceCanvas,
aitjcizeSpectra6Palette,
{
intent: "readable",
},
);
await ditherImage(sourceCanvas, calibratedCanvas, {
...readableSuggestion.ditherOptions,
palette: aitjcizeSpectra6Palette,
});
Custom hardware palettes need calibrated colors and native device colors in the same entry. The name field keeps the display role aligned across conversion and export.
const shelfLabelPalette = [
{ name: "black", color: "#181a1d", deviceColor: "#000000" },
{ name: "white", color: "#d7d9d4", deviceColor: "#FFFFFF" },
{ name: "red", color: "#7d1f1f", deviceColor: "#FF0000" },
{ name: "yellow", color: "#c7b72b", deviceColor: "#FFFF00" },
];
await ditherImage(sourceCanvas, calibratedCanvas, {
palette: shelfLabelPalette,
colorMatching: "lab",
});
replaceColors(calibratedCanvas, deviceCanvas, shelfLabelPalette);
A preview-only tool may not need native device color replacement. A plain hex array gives you a quick dithered preview for controlled palettes.
await ditherImage(sourceCanvas, calibratedCanvas, {
palette: ["#111111", "#f2f0e8", "#c52a24"],
ditheringType: "ordered",
orderedDitheringType: "bayer",
orderedDitheringMatrix: [4, 4],
});
Photos with washed-out highlights or crushed shadows may need tone shaping before palette matching. Tone mapping changes brightness, saturation, and curve response before the dithering step.
await ditherImage(sourceCanvas, calibratedCanvas, {
palette: aitjcizeSpectra6Palette,
toneMapping: {
mode: "scurve",
exposure: 1.08,
saturation: 1.25,
strength: 0.7,
shadowBoost: 0.08,
highlightCompress: 1.25,
midpoint: 0.5,
},
});
Limited-color panels often need lightness compression for photo content. Dynamic range compression remaps LAB lightness into the target display range.
await ditherImage(sourceCanvas, calibratedCanvas, {
palette: aitjcizeSpectra6Palette,
dynamicRangeCompression: {
mode: "auto",
strength: 0.85,
lowPercentile: 0.01,
highPercentile: 0.99,
},
});
Server-side image conversion fits batch exports, admin uploads, and scheduled frame updates. Node.js projects need node-canvas because EPD Optimize expects Canvas-compatible objects.
import { createCanvas, loadImage } from "canvas";
import { writeFile } from "node:fs/promises";
import {
ditherImage,
replaceColors,
acepPalette,
} from "epdoptimize";
const photo = await loadImage("./uploads/gallery-photo.jpg");
const sourceCanvas = createCanvas(photo.width, photo.height);
const calibratedCanvas = createCanvas(photo.width, photo.height);
const deviceCanvas = createCanvas(photo.width, photo.height);
const sourceContext = sourceCanvas.getContext("2d");
// Draw the file into a Canvas-compatible source.
sourceContext.drawImage(photo, 0, 0);
await ditherImage(sourceCanvas, calibratedCanvas, {
palette: acepPalette,
processingPreset: "dynamic",
ditheringType: "errorDiffusion",
errorDiffusionMatrix: "stucki",
});
replaceColors(calibratedCanvas, deviceCanvas, acepPalette);
// Save the device-color output as a PNG file.
await writeFile("./exports/epaper-output.png", deviceCanvas.toBuffer("image/png"));
A browser upload tool needs image loading, canvas sizing, and error handling before dithering starts. This example processes a selected image file and writes both preview and device-color output canvases.
<input id="poster-file" type="file" accept="image/*" /> <canvas id="poster-source"></canvas> <canvas id="poster-preview"></canvas> <canvas id="poster-export"></canvas>
import {
ditherImage,
replaceColors,
aitjcizeSpectra6Palette,
suggestCanvasProcessingOptions,
} from "epdoptimize";
const fileInput = document.querySelector("#poster-file");
const sourceCanvas = document.querySelector("#poster-source");
const previewCanvas = document.querySelector("#poster-preview");
const exportCanvas = document.querySelector("#poster-export");
const sourceContext = sourceCanvas.getContext("2d");
fileInput.addEventListener("change", async (event) => {
const [file] = event.target.files;
if (!file) {
return;
}
const objectUrl = URL.createObjectURL(file);
const image = new Image();
image.addEventListener("load", async () => {
// Match all canvases to the real image dimensions.
sourceCanvas.width = image.naturalWidth;
sourceCanvas.height = image.naturalHeight;
previewCanvas.width = image.naturalWidth;
previewCanvas.height = image.naturalHeight;
exportCanvas.width = image.naturalWidth;
exportCanvas.height = image.naturalHeight;
// Draw the selected image before EPD Optimize reads the pixels.
sourceContext.drawImage(image, 0, 0);
const suggestion = suggestCanvasProcessingOptions(
sourceCanvas,
aitjcizeSpectra6Palette,
{
intent: "faithful",
},
);
await ditherImage(sourceCanvas, previewCanvas, {
...suggestion.ditherOptions,
palette: aitjcizeSpectra6Palette,
});
replaceColors(previewCanvas, exportCanvas, aitjcizeSpectra6Palette);
URL.revokeObjectURL(objectUrl);
});
image.addEventListener("error", () => {
URL.revokeObjectURL(objectUrl);
console.error("Selected image could not load.");
});
image.src = objectUrl;
});
EPD Optimize includes combined palette exports. Each entry contains calibrated display colors and native device colors.
import {
defaultPalette,
gameboyPalette,
spectra6legacyPalette,
spectra6Palette,
aitjcizeSpectra6Palette,
acepPalette,
} from "epdoptimize";
defaultPalette is a black-and-white palette. aitjcizeSpectra6Palette targets Spectra 6 and is the preferred Spectra 6 export. spectra6Palette and spectra6legacyPalette exist for compatibility, but they are not recommended for new Spectra 6 workflows.
palette (string | string[] | Array<{ name: string; color: string; deviceColor: string }>): Sets the palette for quantization. Use built-in combined palette exports for device-ready output.processingPreset (string): Applies preset processing options. Supported values are balanced, dynamic, vivid, soft, and grayscale.ditheringType (string): Selects the main conversion mode. Supported values are errorDiffusion, ordered, random, and quantizationOnly.errorDiffusionMatrix (string): Sets the error diffusion kernel. Supported values include floydSteinberg, atkinson, falseFloydSteinberg, jarvis, stucki, burkes, sierra3, sierra2, and sierra2-4a.algorithm (string): Provides a backwards-compatible alias for errorDiffusionMatrix.serpentine (boolean): Alternates scan direction on each row during error diffusion.orderedDitheringType (string): Sets the ordered dithering type. The available value is bayer.orderedDitheringMatrix ([number, number]): Sets the Bayer matrix size for ordered dithering.randomDitheringType (string): Sets the random dithering mode. Supported values are blackAndWhite and rgb.colorMatching (string): Selects the palette distance model. Supported values are rgb and lab.toneMapping (object): Applies exposure, saturation, contrast, or S-curve preprocessing before palette matching.toneMapping.mode ("off" | "contrast" | "scurve"): Selects the tone mapping mode.toneMapping.exposure (number): Multiplies brightness before tone shaping.toneMapping.saturation (number): Multiplies color saturation before palette matching.toneMapping.contrast (number): Sets the contrast multiplier for contrast mode.toneMapping.strength (number): Controls S-curve intensity.toneMapping.shadowBoost (number): Lifts darker values in S-curve mode.toneMapping.highlightCompress (number): Compresses bright values in S-curve mode.toneMapping.midpoint (number): Sets the S-curve midpoint.dynamicRangeCompression (object | boolean): Remaps LAB lightness into the display palette range.dynamicRangeCompression.mode ("off" | "display" | "auto"): Disables compression, compresses into the palette lightness range, or uses percentile clipping before compression.dynamicRangeCompression.strength (number): Controls the compression amount.dynamicRangeCompression.lowPercentile (number): Sets the low percentile for auto compression.dynamicRangeCompression.highPercentile (number): Sets the high percentile for auto compression.levelCompression (object): Applies an optional legacy or preprocessing range remap.levelCompression.mode ("perChannel" | "luma"): Selects per-channel or luma-based range remapping.sampleColorsFromImage (boolean): Reserves support for image-derived palettes.numberOfSampleColors (number): Sets the number of colors to sample for image-derived palettes.import {
ditherImage,
replaceColors,
classifyImageStyle,
classifyCanvasImageStyle,
suggestProcessingOptions,
suggestCanvasProcessingOptions,
getDefaultPalettes,
getDeviceColors,
getDeviceColorsForPalette,
getProcessingPreset,
getProcessingPresetNames,
getProcessingPresetOptions,
aitjcizeSpectra6Palette,
acepPalette,
} from "epdoptimize";
// Dither a Canvas source into calibrated palette colors.
await ditherImage(productPhotoCanvas, calibratedPreviewCanvas, {
palette: aitjcizeSpectra6Palette,
processingPreset: "balanced",
});
// Replace calibrated palette colors with native e-paper device colors.
replaceColors(calibratedPreviewCanvas, deviceExportCanvas, aitjcizeSpectra6Palette);
// Classify raw ImageData as a photo, illustration, or unknown image type.
const imageData = productPhotoContext.getImageData(0, 0, 800, 480);
const classification = classifyImageStyle(imageData, {});
// Classify a browser Canvas or node-canvas object.
const canvasClassification = classifyCanvasImageStyle(productPhotoCanvas, {});
// Suggest processing options from raw ImageData and a target palette.
const imageDataSuggestion = suggestProcessingOptions(imageData, acepPalette, {
intent: "faithful",
});
// Suggest processing options directly from a Canvas source.
const canvasSuggestion = suggestCanvasProcessingOptions(
productPhotoCanvas,
aitjcizeSpectra6Palette,
{
intent: "readable",
},
);
// Return calibrated color values for a named built-in palette.
const calibratedColors = getDefaultPalettes("spectra6");
// Return native device colors for a named built-in palette.
const nativeColors = getDeviceColors("spectra6");
// Return device colors aligned to another palette role order.
const alignedColors = getDeviceColorsForPalette("spectra6", "acep");
// Return the full definition for a processing preset.
const dynamicPreset = getProcessingPreset("dynamic");
// Return all preset names for a settings UI.
const presetNames = getProcessingPresetNames();
// Return value, title, and description objects for preset controls.
const presetControlOptions = getProcessingPresetOptions();
Q: Why does my exported image still use preview colors?
A: The workflow likely skipped replaceColors. Call replaceColors after ditherImage to map calibrated palette colors to native deviceColor values.
Q: Which palette should I use for Spectra 6?
A: Use aitjcizeSpectra6Palette for new Spectra 6 workflows. The spectra6Palette and spectra6legacyPalette exports remain available, but they are not recommended for new conversions.
Q: What is the difference between ditherImage and replaceColors?
A: ditherImage quantizes and dithers the image to the calibrated color values. replaceColors then swaps every calibrated pixel with the matching deviceColor so the final image is ready for the e‑paper panel.
Q: How can I choose the best dithering settings automatically?
A: Call suggestCanvasProcessingOptions with the loaded canvas and the palette. It returns a ditherOptions object you can spread directly into ditherImage. Use the intent parameter to guide the recommendation toward a specific output look.
The post Dither Images for E-Paper Displays in JavaScript – EPD Optimize appeared first on CSS Script.
Flipper Devices has officially announced Flipper One, a fully modular, open-source Linux cyberdeck built on…
Flipper Devices has officially announced Flipper One, a fully modular, open-source Linux cyberdeck built on…
The Lead Off A baby was safely caught after being dropped from a second-floor window…
LANSING, Mich. (WOWO) — A new bill introduced in the Michigan Senate would prohibit minors…
Discord has officially rolled out end-to-end encryption (E2EE) for all voice and video communications across…
A sweeping automated supply chain attack codenamed “Megalodon” struck GitHub on May 18, 2026, injecting…
This website uses cookies.