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 at [email protected].
License Key Management
Lumina uses a two-tier key system 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 derived from your master key (max 24 hours). Safe to send 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 clientThe TTL is capped at 86400 seconds (24 hours) and will never exceed the master key's own expiration.
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
Both master and dependent keys work with the editor — but master keys should never appear in browser code. Use dependent keys for all client-side usage.
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
The simplest way to create an editor is 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).
previewQuality and previewSize control preview image resolution and compression — lower values give faster real-time feedback but reduced fidelity. Previews are returned as JPEG-encoded ArrayBuffers. 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: 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 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: 10,
tint: -5,
} satisfies Partial<Shared.ImageAdjustments>);Undo and Redo
Client API
The Client API maintains a linear undo/redo history. The editor automatically determines which operation to undo or redo based on 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 uses per-operation-type undo/redo stacks (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
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 { 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 — 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, reach out at [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 the complete Client API surface. - See
Core.Editorfor the complete Core API surface. - See
Shared.ErrorCodefor all error codes and their meanings.