Appearance
Quick Start
Installation
bash
npm install @luminaphoto/lumina-jsObtaining a License Key
Lumina JS is in early access. To request a license key, email [email protected].
License Key Management
Lumina uses two kinds of keys to keep your license secure in browser environments.
Master key — the key you receive when purchasing a license. Keep this on your server only. Never expose it in client-side code.
Dependent key — a short-lived key (max 24 hours) that your server derives from your master key. You can send a dependent key to the browser.
Server-Side Key Derivation
Generate a dependent key on your server and serve it to the client (e.g., via an API endpoint):
typescript
import { Licensing } from '@luminaphoto/lumina-js';
// Generate a dependent key valid for 1 hour
const dependentKey = await Licensing.createDependentKey(
process.env.LUMINA_MASTER_KEY,
3600
);
// Return dependentKey to the browser clientcreateDependentKey caps the TTL at 86400 seconds (24 hours) and at the expiration of the master key.
Browser-Side Usage
Pass the dependent key from your server to the editor:
typescript
import { Client } from '@luminaphoto/lumina-js';
const editor = new Client.Editor({
licenseKey: dependentKeyFromServer,
workerPath: '/lumina-js/client/worker.js',
});TIP
The editor accepts both master and dependent keys. Keep master keys out of browser code and use dependent keys on the client.
Choosing an API
Lumina JS exposes two 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. Configure your bundler to emit the worker script to a known path. With Vite:
typescript
// vite.config.ts
export default {
worker: { format: 'es' },
};Then reference the worker script at runtime:
typescript
const workerPath = `${import.meta.env.BASE_URL}lumina-js/client/worker.js`;Configuration
Create an editor with the createEditor factory function:
typescript
import { createEditor } from '@luminaphoto/lumina-js';
const editor = createEditor({
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
});You can also import the class directly via new Client.Editor(config).
previewSize sets the resolution and previewQuality sets the JPEG compression of preview images. Lower values give faster real-time feedback at reduced fidelity. The editor returns previews as JPEG-encoded ArrayBuffers, and export uses the original resolution.
Initialization
typescript
await editor.initialize();initialize() starts the worker and loads the WASM module, and must complete before any other call. initialize() throws Shared.LuminaError on failure (bad license key, worker fails to load, 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
Each operation has a matching preview* method that renders without adding a history entry. Call preview* while the user moves a slider, and call the matching apply* method when the user releases the slider.
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' }));
});Debounce preview calls. The WASM pipeline processes one operation at a time, and extra calls wait in a queue.
Available Operations
The Client and Core editors expose the same operations with the same parameters.
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: each -100 to +100
await editor.previewTemperature(15, -10);
await editor.applyTemperature(15, -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 6 points. startY and endY set the endpoint values separately from the fitted curve.
Color Grading
Color grading uses 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. applyAdjustments applies only the keys you include.
typescript
import { Shared } from '@luminaphoto/lumina-js';
await editor.applyAdjustments({
exposure: 0.3,
highlights: -20,
shadows: 15,
temperature: 10,
tint: -5,
} satisfies Partial<Shared.ImageAdjustments>);Undo and Redo
Client API
The Client API maintains a linear undo/redo history. The editor picks the operation to undo or redo by chronological order.
typescript
// 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();
const { imageData: afterRedo } = await editor.redo();Core API
The Core API keeps a separate undo/redo stack for each operation type (depth: 20 per operation). You must specify which operation type to undo or redo.
typescript
import { Shared } from '@luminaphoto/lumina-js';
if (editor.canUndo(Shared.NodeType.Exposure)) {
const { imageData } = editor.undoOperation(Shared.NodeType.Exposure);
}
if (editor.canRedo(Shared.NodeType.Exposure)) {
const { imageData } = editor.redoOperation(Shared.NodeType.Exposure);
}Available NodeType values: Exposure · Brightness · Contrast · Saturation · Temperature · HighlightsShadows · TonalCurve · ColorGrading
History Manager
editor.historyManager returns the full log of operations:
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);exportImage() processes at the full original resolution, regardless of previewSize.
Resetting to Original
reset() 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 to access WASM directly: for synchronous operations, to manage your own workers or to run outside the browser.
typescript
import { createCoreEditor } from '@luminaphoto/lumina-js';
const editor = createCoreEditor({
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. The 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. If you need help, email [email protected].
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 all methods of the Client API. - See
Core.Editorfor all methods of the Core API. - See
Shared.ErrorCodefor all error codes and their meanings.