
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.
Features:
- Convert images into calibrated e-paper palette colors.
- Export native device colors for display output.
- Support browser Canvas and Node.js workflows.
- Process Spectra 6, AcEP, and custom palettes.
- Apply error diffusion, ordered dithering, random dithering, or plain quantization.
- Adjust exposure, saturation, contrast, and S-curve tone response.
- Compress image lightness into the target display range.
- Match colors in RGB or LAB space.
- Suggest processing settings from image analysis.
- Generate dither previews before device color replacement.
Use Cases:
- Pre‑process images for an e‑ink digital picture frame that uses a Spectra 6 or ACeP display.
- Build a browser tool that lets users upload photos and preview exactly how they will look on a specific e‑paper panel.
- Create a custom palette for a non‑standard color e‑paper display by measuring its actual color output.
- Auto‑convert mixed photo and illustration galleries with a single processing pipeline that adjusts settings per image.
How To Use It:
Installation
Install EPD Optimize with NPM.
npm install epdoptimize
Basic Usage
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);
Real-world Examples
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;
});
Built-In Palette Imports
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.
Configuration Options
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 arebalanced,dynamic,vivid,soft, andgrayscale.ditheringType(string): Selects the main conversion mode. Supported values areerrorDiffusion,ordered,random, andquantizationOnly.errorDiffusionMatrix(string): Sets the error diffusion kernel. Supported values includefloydSteinberg,atkinson,falseFloydSteinberg,jarvis,stucki,burkes,sierra3,sierra2, andsierra2-4a.algorithm(string): Provides a backwards-compatible alias forerrorDiffusionMatrix.serpentine(boolean): Alternates scan direction on each row during error diffusion.orderedDitheringType(string): Sets the ordered dithering type. The available value isbayer.orderedDitheringMatrix([number, number]): Sets the Bayer matrix size for ordered dithering.randomDitheringType(string): Sets the random dithering mode. Supported values areblackAndWhiteandrgb.colorMatching(string): Selects the palette distance model. Supported values arergbandlab.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.
API Methods
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();
Alternatives:
- Dithering Images On The Client Side – as-dithered-image.js
- Convert Images to Pixel Art with JS Dithering & Custom Palettes – Image-to-Pixel
- Fast Gaussian Blur for Canvas Image in JavaScript – glur
- Manipulate Images Pixel By Pixel Using JavaScript And Canvas – pixelmap
FAQs:
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.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
