Appearance
Quick Start
Installation
bash
npm install @luminaphoto/lumina-jsObtaining a License Key
Lumina JS is in early access. To request a license key, reach out directly via GitHub.
Choosing an API
Lumina JS exposes two distinct APIs:
| Client API | Core API | |
|---|---|---|
| Execution | Web Worker (off main thread) | Main thread or custom worker |
| Async model | All methods async | Mix of sync and async |
| Best for | Browser applications, UI responsiveness | Specialized pipelines, Node.js, custom workers |
| Undo/redo | ✅ | ✅ |
| Dispose | await editor.dispose() | editor.dispose() |
For most browser applications, use the Client API.
Client API
Worker Setup
The Client API runs WASM inside a Web Worker. Your bundler needs to emit the worker script to a known path. With Vite:
typescript
// vite.config.ts
export default {
worker: { format: 'es' },
};Then reference it at runtime:
typescript
const workerPath = `${import.meta.env.BASE_URL}lumina-js/client/worker.js`;Configuration
typescript
import { Client } from '@luminaphoto/lumina-js';
const editor = new Client.Editor({
licenseKey: 'YOUR_LICENSE_KEY', // required
workerPath: workerPath, // path to the bundled worker script
autoInitialize: false, // default: true
enableLogging: false, // default: false
timeout: 30000, // ms, default: 30000
previewQuality: 85, // JPEG quality 1–100, default: 100
previewSize: 1024, // max preview dimension in px, default: 1024
});previewQuality and previewSize control preview image resolution and compression — lower values give faster real-time feedback but reduced fidelity. Full export always uses original resolution.
Initialization
typescript
await editor.initialize();Starts the worker and loads the WASM module. Must complete before any other call. Throws Shared.LuminaError on failure (bad license key, worker load failure, etc.).
Loading Images
typescript
const buffer = await file.arrayBuffer();
const metadata = await editor.loadImage(buffer, 'raw');
console.log(`${metadata.width}×${metadata.height}, temp: ${metadata.temperature}K`);
// Get the initial preview after loading
const { imageData } = await editor.getPreview();
const url = URL.createObjectURL(new Blob([imageData], { type: 'image/jpeg' }));Supported input formats: 'jpeg' · 'png' · 'webp' · 'raw' (DNG, CR2, NEF, ARW)
loadImage returns image dimensions and, for RAW files, the embedded white balance metadata (temperature, tint). Call getPreview() afterwards to get the initial rendered preview.
Detecting Format from File Extension
typescript
function detectFormat(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
if (['jpg', 'jpeg'].includes(ext)) return 'jpeg';
if (ext === 'png') return 'png';
if (ext === 'webp') return 'webp';
if (['dng', 'raw', 'cr2', 'nef', 'arw'].includes(ext)) return 'raw';
return 'jpeg';
}Real-time Preview
Every operation has a matching preview* method that renders without committing to history. Use these while a slider is moving, then call the apply* method when the user releases it.
typescript
// Debounce during drag — show live feedback without flooding the worker
slider.addEventListener('input', debounce(async () => {
const { imageData } = await editor.previewExposure(slider.valueAsNumber);
img.src = URL.createObjectURL(new Blob([imageData], { type: 'image/jpeg' }));
}, 150));
// Commit on release — adds to undo/redo history
slider.addEventListener('change', async () => {
await editor.applyExposure(slider.valueAsNumber);
const { imageData } = await editor.getPreview();
img.src = URL.createObjectURL(new Blob([imageData], { type: 'image/jpeg' }));
});Debouncing previews is important — the WASM pipeline processes one operation at a time and stacked calls will queue up.
Available Operations
All operations are available on both Client and Core editors. Parameters are identical.
Global Light
typescript
// Exposure: -5.0 to +5.0 EV
await editor.previewExposure(1.2);
await editor.applyExposure(1.2);
// Brightness: -100 to +100
await editor.previewBrightness(20);
await editor.applyBrightness(20);
// Contrast: -100 to +100
await editor.previewContrast(-15);
await editor.applyContrast(-15);Regional Light
typescript
// Highlights, Shadows, Midtones: each -100 to +100
await editor.previewHighlightsShadows(-30, 25, 0);
await editor.applyHighlightsShadows(-30, 25, 0);Color
typescript
// Saturation: -100 to +100
await editor.previewSaturation(15);
await editor.applySaturation(15);
// Temperature delta (relative to base), Tint: -100 to +100
await editor.previewTemperature(200, -10);
await editor.applyTemperature(200, -10);Tonal Curve
typescript
import { Shared } from '@luminaphoto/lumina-js';
const sCurve: Shared.TonalCurvePoints = {
startY: 0.0, // black point
endY: 1.0, // white point
middlePoints: [ // interior control points, x and y in [0, 1]
{ x: 0.25, y: 0.20 },
{ x: 0.75, y: 0.80 },
],
};
await editor.previewTonalCurve(sCurve);
await editor.applyTonalCurve(sCurve);middlePoints can contain up to 8 points. startY and endY pin the endpoints independently of the curve fit.
Color Grading
Three independent color wheels — one per tonal range.
typescript
import { Shared } from '@luminaphoto/lumina-js';
const grade: Shared.ColorGradingParams = {
shadowHue: 240, // 0–360
shadowSaturation: 0.25, // 0–1
shadowBuffer: 0.10, // blend strength, 0–0.5
midtoneHue: 0,
midtoneSaturation: 0,
midtoneBuffer: 0,
highlightHue: 30,
highlightSaturation: 0.20,
highlightBuffer: 0.10,
};
await editor.previewColorGrading(grade);
await editor.applyColorGrading(grade);Batch Adjustments
Apply several adjustments in a single call. Only keys you include are applied.
typescript
import { Shared } from '@luminaphoto/lumina-js';
await editor.applyAdjustments({
exposure: 0.3,
highlights: -20,
shadows: 15,
temperature: 150,
tint: -5,
} satisfies Partial<Shared.ImageAdjustments>);Undo and Redo
Each operation type has its own independent undo/redo stack (depth: 20).
typescript
import { Shared } from '@luminaphoto/lumina-js';
// Check availability
const canUndo = editor.canUndo();
const canRedo = editor.canRedo();
// Undo / redo — both return a preview of the resulting state
const { imageData: afterUndo } = await editor.undo(Shared.NodeType.Exposure);
const { imageData: afterRedo } = await editor.redo(Shared.NodeType.Exposure);Available NodeType values: Exposure · Brightness · Contrast · Saturation · Temperature · HighlightsShadows · TonalCurve · ColorGrading
History Manager
The history manager gives access to the full operation log:
typescript
const entries = editor.historyManager.getAllEntries();
// HistoryEntry: { id, operationType, payload, timestamp }
entries.forEach(entry => {
console.log(`${entry.operationType} at ${new Date(entry.timestamp).toLocaleTimeString()}`);
console.log(entry.payload);
});
const currentIndex = editor.historyManager.getCurrentIndex();Export
typescript
// Formats: 'jpeg' | 'png' | 'webp'
// Quality: 1–100 (ignored for png)
const { data } = await editor.exportImage('jpeg', 95);
const blob = new Blob([data], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'edited.jpg';
a.click();
URL.revokeObjectURL(url);Export always processes at full original resolution, regardless of previewSize.
Resetting to Original
Reverts all applied operations and returns a preview of the original image:
typescript
const { imageData } = await editor.reset();
img.src = URL.createObjectURL(new Blob([imageData], { type: 'image/jpeg' }));Core API
Use the Core API for direct WASM access — synchronous operations, custom worker management, or non-browser environments.
typescript
import { Core } from '@luminaphoto/lumina-js';
const editor = new Core.Editor({
licenseKey: 'YOUR_LICENSE_KEY',
previewQuality: 100,
});
await editor.initialize();
const buffer = await fetch('photo.jpg').then(r => r.arrayBuffer());
await editor.loadImage(buffer, 'jpeg');
// Preview and apply are synchronous
const { imageData } = editor.previewBrightness(20);
editor.applyBrightness(20);
editor.applyContrast(-10);
editor.applyExposure(0.3);
const { data } = editor.exportImage('webp', 90);
editor.dispose();Error Handling
All operations throw Shared.LuminaError on failure.
typescript
import { Client, Shared } from '@luminaphoto/lumina-js';
try {
const editor = new Client.Editor({ licenseKey: 'YOUR_LICENSE_KEY' });
await editor.initialize();
await editor.loadImage(buffer, 'jpeg');
} catch (error) {
if (error instanceof Shared.LuminaError) {
switch (error.code) {
case Shared.ErrorCode.INVALID_LICENSE_KEY:
showError('Invalid license key.');
break;
case Shared.ErrorCode.LICENSE_EXPIRED:
showError('License expired.');
break;
case Shared.ErrorCode.IMAGE_LOAD_FAILED:
showError('Failed to load image — file may be corrupted or unsupported.');
break;
case Shared.ErrorCode.UNSUPPORTED_FORMAT:
showError('Unsupported image format.');
break;
default:
showError(`Error ${error.code}: ${error.message}`);
}
}
}error.details may contain additional context for debugging.
Browser Requirements
| Feature | Minimum version |
|---|---|
| WebAssembly | Chrome 69+, Firefox 79+, Safari 14+, Edge 79+ |
| Web Workers | Required for Client API |
| ES Modules | Required |
Next Steps
- Browse the full API Reference for complete method signatures and parameter documentation.
- See
Client.Editorfor the complete Client API surface. - See
Core.Editorfor the complete Core API surface. - See
Shared.ErrorCodefor all error codes and their meanings.