Skip to content
Open
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
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.github
node_modules
dist
npm-debug.log*
examples/rating-data/pairwise-votes.jsonl
examples/rating-data/pairwise-votes.json
92 changes: 92 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copilot instructions for `epdoptimize`

## Build, typecheck, and validation commands

Run from repository root:

```bash
npm ci
npm run build
npm run typecheck
```

Library and examples:

```bash
# Build JS bundles + .d.ts (published output)
npm run build

# Build only JS bundles with Vite
npm run build:js

# Build only TypeScript declarations
npm run build:types

# Build examples site into dist/examples
npm run build:examples

# Run examples/dev playground (Fabric + rating tool)
npm run examples:dev
```

WASM-specific path (only when changing WASM error diffusion code):

```bash
npm run build:wasm
```

There is currently **no `npm test` script** and **no lint script** in `package.json`, so there is no built-in single-test command in this repository today.

## High-level architecture

### 1) Public API surface (`src/index.ts`)

`src/index.ts` is the single export surface. It re-exports:

- Palette helpers and built-in palette constants
- Main pipeline APIs: `ditherImage`, `ditherCanvas`, `applyImageAdjustments*`
- Auto recommendation APIs from `src/auto-processing.ts`
- Image classification APIs from `src/image-style.ts`
- `replaceColors` final device-color mapping

When adding/removing APIs, wire them through `src/index.ts` so package consumers can import them.

### 2) Image pipeline split (`src/dither/dither.ts` + `src/dither/processing.ts`)

The core flow is intentionally staged:

1. **Adjustment stage** (`applyImageProcessing` in `processing.ts`): paper normalization, clarity, tone mapping, dynamic range compression, level compression.
2. **Dither stage** (`ditherImageData` in `dither.ts`): quantization / ordered / random / error diffusion / utility-based algorithms.
3. **Optional post-processing**: edge preservation and edge antialiasing cleanup.
4. **Final mapping stage** (`src/replaceColors/replaceColors.ts`): calibrated palette colors -> device colors.

`ditherImage()` runs adjustments + dithering; `ditherCanvas()` only runs dithering; `applyImageAdjustments*()` only runs the adjustment stage.

### 3) Engine selection and fallbacks

- `processingEngine: "wasm" | "auto"` currently accelerates **RGB error diffusion only**; unsupported combinations fall back to JS.
- `adjustmentEngine: "auto"` can offload adjustment work to a Worker for large images (`src/dither/adjustment-async.ts`).
- Worker code (`src/dither/adjustment-worker.ts`) forces `adjustmentEngine: "js"` inside the worker path to avoid nested worker recursion.

### 4) Auto recommendation subsystem (`src/image-style.ts` + `src/auto-processing.ts`)

- `classifyImageStyle` computes heuristics/metrics and emits `kind` + confidence.
- Auto APIs build recommended `ditherOptions` from classification + palette profile + intent.
- Two strategies exist: `"legacy"` and `"layered"`.
- Split helpers (`suggestCanvasImageAdjustmentOptions`, `suggestCanvasDitherOptions`) are used for editor workflows that keep image-stage and canvas-stage settings separate.

### 5) Palette model and role ordering

- Default palette data lives in `src/dither/data/default-palettes.json`.
- Palette entries are `{ name, color, deviceColor }`.
- `color` is the calibrated display appearance for dithering; `deviceColor` is the native panel color for export.
- `src/dither/functions/palette-order.ts` enforces canonical role ordering (black/gray/white/colors), and palette keys are normalized case-insensitively.

## Key repository conventions

- **Use combined palette entries** (`{ name, color, deviceColor }`) as the primary format. The legacy `{ originalColors, replaceColors }` shape is only for backward compatibility.
- **Keep adjustment and dithering concerns separate** in editor integrations (also documented in `FABRIC_FILTER_README.md`): per-image adjustment controls first, whole-canvas dithering/export second.
- **Preserve preset key behavior** in `PROCESSING_PRESETS`: lookup is lowercase (`getProcessingPreset(String(name).toLowerCase())`), while preset `name` values keep public API casing (for example `posterScan` name with `posterscan` key).
- **Do not bypass role-aware palette alignment** when mixing calibration and device palettes; use existing palette-order helpers so role mapping remains stable.
- **`replaceColors` expects exact calibrated color matches** in source pixels and warns when pixels cannot be replaced; do not introduce fuzzy matching without explicit design changes.
- **Examples app routing/deploy assumes `/epdoptimize/` base** (`examples/vite.config.js`), and the rating tool persists votes to `examples/rating-data/pairwise-votes.jsonl` (+ JSON snapshot).
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM node:24-alpine AS build
WORKDIR /app

COPY package.json package-lock.json ./
COPY examples ./examples
COPY src ./src
COPY tsconfig.json tsconfig.build.json ./
COPY vite.config.js ./

RUN npm ci

ARG EXAMPLES_BASE_PATH=/
ENV EXAMPLES_BASE_PATH=${EXAMPLES_BASE_PATH}
RUN npm run build:examples:docker

FROM nginx:1.29-alpine AS runtime
COPY docker/nginx-editor.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist/examples /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ Heuristically classify the image, score presets, and suggest dither options for
npm install epdoptimize
```

## Self-host the examples web app with Docker

This repository includes a Docker setup for the examples web app
(`examples/index.html`, `examples/fabric-filter.html`, `examples/rating-tool.html`).

Build examples assets locally:

```bash
npm ci
npm run build:examples:docker
```

Run with Docker:

```bash
docker build -t epdoptimize-examples .
docker run --rm -p 8080:80 epdoptimize-examples
```

Then open:

```txt
http://localhost:8080/
```

Run with Compose:

```bash
docker compose up --build
```

The Docker build defaults to root hosting (`/`). If you need a subpath build,
set `EXAMPLES_BASE_PATH` at build time:

```bash
docker build --build-arg EXAMPLES_BASE_PATH=/my-path/ -t epdoptimize-examples .
```

## Quick Start

```html
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
examples-app:
build:
context: .
dockerfile: Dockerfile
args:
EXAMPLES_BASE_PATH: /
ports:
- "8080:80"
17 changes: 17 additions & 0 deletions docker/nginx-editor.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
server {
listen 80;
server_name _;

root /usr/share/nginx/html;
index index.html;

location / {
try_files $uri $uri/ /index.html;
}

location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ {
expires 7d;
add_header Cache-Control "public";
try_files $uri =404;
}
}
37 changes: 37 additions & 0 deletions examples/vite.docker.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineConfig } from "vite";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const normalizeBasePath = (value) => {
if (!value) return "/";
const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
return withLeadingSlash.endsWith("/")
? withLeadingSlash
: `${withLeadingSlash}/`;
};

export default defineConfig({
root: __dirname,
base: normalizeBasePath(process.env.EXAMPLES_BASE_PATH ?? "/"),
resolve: {
alias: {
epdoptimize: path.resolve(__dirname, "../src"),
},
},
build: {
outDir: path.resolve(__dirname, "../dist/examples"),
rollupOptions: {
input: {
demo: path.resolve(__dirname, "index.html"),
fabricFilter: path.resolve(__dirname, "fabric-filter.html"),
ratingTool: path.resolve(__dirname, "rating-tool.html"),
},
},
emptyOutDir: true,
},
server: {
open: true,
},
});
35 changes: 35 additions & 0 deletions examples/vite.editor.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineConfig } from "vite";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const normalizeBasePath = (value) => {
if (!value) return "/";
const withLeadingSlash = value.startsWith("/") ? value : `/${value}`;
return withLeadingSlash.endsWith("/")
? withLeadingSlash
: `${withLeadingSlash}/`;
};

export default defineConfig({
root: __dirname,
base: normalizeBasePath(process.env.EDITOR_BASE_PATH ?? "/"),
resolve: {
alias: {
epdoptimize: path.resolve(__dirname, "../src"),
},
},
build: {
outDir: path.resolve(__dirname, "../dist/editor"),
rollupOptions: {
input: {
editor: path.resolve(__dirname, "fabric-filter.html"),
},
},
emptyOutDir: true,
},
server: {
open: "/fabric-filter.html",
},
});
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
"build": "npm run build:js && npm run build:types",
"build:js": "vite build",
"build:types": "tsc -p tsconfig.build.json",
"build:editor": "vite build --config examples/vite.editor.config.js",
"build:wasm": "mkdir -p experiments/wasm && asc wasm-src/error-diffusion-rgb.ts -o experiments/wasm/error-diffusion-rgb.wasm -O --noAssert --runtime stub --enable simd",
"bench:wasm": "npm run build && node scripts/bench-wasm-error-diffusion.mjs",
"generate:sample-previews": "./scripts/generate-sample-previews.sh",
"typecheck": "tsc -p tsconfig.json --noEmit",
"prepack": "npm run build",
"editor:dev": "vite --config examples/vite.editor.config.js",
"examples:dev": "vite --config examples/vite.config.js",
"build:examples": "vite build --config examples/vite.config.js && cp examples/example-dither.jpg dist/examples/example-dither.jpg",
"build:examples:docker": "vite build --config examples/vite.docker.config.js && cp examples/example-dither.jpg dist/examples/example-dither.jpg",
"predeploy": "npm run build:examples",
"deploy": "npx gh-pages -d dist/examples",
"release": "npm run release:patch",
Expand Down