Lens

Reference

API reference

Everything exported from @lens-image/core and @lens-image/react.


ImageOptimizer

Construct one per configuration and reuse it. It memoises engine detection and owns the result cache, so a fresh instance per request throws both away.

const optimizer = new ImageOptimizer({
  adapter,
  engine,
  quality: 80,
  formats: ['webp'],
  sizes: [{ width: 1200 }],
  fallbackFormat: 'jpeg',
  key: '{hash}/{name}-{label}.{ext}',
  prefix: '',
  preserveMetadata: false,
  autoOrient: true,
  concurrency: 4,
  validate: { maxBytes: 25 * 1024 * 1024 },
  retry: { attempts: 3 },
  cache: false,
  cacheControl: 'public, max-age=31536000, immutable',
  allowRemote: false,
  onWarning: (warning) => logger.warn(warning),
});

optimize(options)

Everything except adapter, engine, cache and allowRemote can be overridden per call.

await optimizer.optimize({
  source: './photo.jpg',
  filename: 'hero.jpg',
  formats: ['avif', 'webp', 'jpg'],
  sizes: [{ width: 1200 }, { width: 600, fit: 'contain' }],
  thumbnail: { width: 128, format: 'webp' },
  metadata: { uploadedBy: 'user-42' },
  keepData: false,
  signal: controller.signal,
  force: false,
});

Other methods

Method
inspect(source)Metadata only. Nothing is encoded or uploaded.
optimizeMany(sources, opts)Sequential by design, each call already fans out internally, and overlapping whole images multiplies peak memory.
delete(result)Removes every variant. Needs an adapter implementing remove.
dispose()Releases adapter resources. Safe to call twice.
engine()Resolves the codec backend, running detection on first use.

The result

interface OptimizeResult {
  id: string;             // stable per (source, options), also the cache key
  runId: string;          // unique per call, for log correlation
  source: ImageMetadata & { filename: string; checksum: string };
  formats: { webp?: FormatResult; jpeg?: FormatResult; /* … */ };
  variants: Variant[];    // flat, in plan order
  thumbnail?: Variant;
  warnings: LensWarning[];
  totalSize: number;
  savings: number;        // 0-1, against the largest single output
  durationMs: number;
  cached: boolean;
  engine: string;
  adapter: string | null;
}
interface FormatResult {
  format: ImageFormat;
  urls: Record<string, string>;   // { '600w': 'https://…', '1200w': 'https://…' }
  variants: Variant[];
  size: number;                   // total bytes across this format
  srcset: string;
  largest: Variant;
  smallest: Variant;
}

Engines

Factory
createSharpEngine(opts)sharp-backed. Lazy dynamic import, nothing loads until you encode.
createPassthroughEngine()No codec. Validates, hashes, keys and stores originals. Ideal for tests.
detectEngine()sharp if available, otherwise passthrough. The default.
import { createSharpEngine, createPassthroughEngine, detectEngine } from '@lens-image/core';

new ImageOptimizer({
  engine: createSharpEngine({
    threads: 1,      // Lens already parallelises; 4 jobs x 4 libvips threads = 16
    cache: false,    // libvips' global cache leaks in a long-lived server
    pixelLimit: 100_000_000,
  }),
});

Upload handler

Option
optimizerRequired.
fieldNameMultipart field to read. Default 'file'.
maxFilesDefault 1. Over the limit is a 413.
authorize(request)Return a Response to short-circuit before any work happens.
optionsPer-request optimize options, or a function of the request. The right place to turn a session into a key prefix.
serialize(results)Shape the success payload.
onError(error, request)Observe failures. Unexpected errors return a generic message to the client; the real one comes here.

@lens-image/core/browser

A subset with no node: imports at all, so it bundles for a browser, a Worker or an edge runtime. A CI check walks the import graph and fails if anything Node-specific creeps in.

import { sniff, resolveValidation, validateImage, LensError } from '@lens-image/core/browser';

const policy = resolveValidation({ maxBytes: 10_000_000, allowedFormats: ['jpeg', 'png'] });
const header = new Uint8Array(await file.slice(0, 4096).arrayBuffer());

try {
  validateImage(sniff(header, file.size), policy, file.name);
} catch (error) {
  if (LensError.is(error)) showToast(error.message);
}

Note the file.slice(0, 4096), only the header is read, so validating a 40 MB file costs four kilobytes.

HTML helpers

Function
toPicture(result, opts)<picture>-ready sources and an img fallback, with intrinsic dimensions always set.
pickVariant(result, w, fmt)Smallest variant at least that wide, never an upscale.
buildSrcset(variants)A sorted srcset string from any variant list.

@lens-image/react

useImageUpload(options)

const {
  items,        // every tracked file: status, progress, result, error, previewUrl
  images,       // successful results only
  isUploading,
  progress,     // 0-100, weighted by byte count
  error,        // first error in the queue
  status,       // 'idle' | 'uploading' | 'success' | 'error' | 'cancelled'
  upload,       // (files: FileList | File[] | File) => Promise<UploadedImage[]>
  cancel,
  reset,
  remove,       // (id: string) => void
} = useImageUpload({ endpoint: '/api/upload' });

Options: endpoint (required), fieldName, headers, fields, credentials, batch, maxBytes, accept, maxFiles, preview, parseResponse, onSuccess, onError, onComplete.

useDropzone(options)

Returns getRootProps, getInputProps, isDragActive and open. Prop getters, not components spread them onto markup you are already styling. The drag state is tracked with an enter/leave depth counter so it does not flicker as the cursor crosses child elements.

Helpers

toPictureProps, pickVariant, validateFile, matchesAccept, formatBytes. All pure, so they are safe to run during SSR. The two hooks are client-only.

Exports for adapter authors

sniff, mimeTypeFor, extensionFor, buildKey, joinKey, slugify, withRetry, isRetryableError, mapLimit, sha256, shortHash, LensError.