Skip to content

Quick Start

Installation

bash
npm install @luminaphoto/lumina-js

Obtaining 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 APICore API
ExecutionWeb Worker (off main thread)Main thread or custom worker
Async modelAll methods asyncMix of sync and async
Best forBrowser applications, UI responsivenessSpecialized pipelines, Node.js, custom workers
Undo/redo
Disposeawait 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

FeatureMinimum version
WebAssemblyChrome 69+, Firefox 79+, Safari 14+, Edge 79+
Web WorkersRequired for Client API
ES ModulesRequired

Next Steps

Proprietary. All rights reserved.