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, 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 client

createDependentKey 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 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. 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 ​

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

Next Steps ​

Proprietary. All rights reserved.