faceless-photolib

@faceless-photolib/lut

3D LUT parsing (.cube / .3dl / ACES .clf) and CPU-reference tetrahedral/trilinear interpolation, lowering every format to one rgba16float-shaped cube descriptor for the faceless-photolib engine.

3D LUT support for faceless-photolib — a headless, color-managed, GPU-accelerated image-editing engine.

Parses the three on-disk LUT formats — .cube (R-fastest float), .3dl (B-fastest int), and ACES .clf (XML ProcessList) — each with its own index logic, and lowers them to one backend-agnostic Lut3dDescriptor (a flat size³ × 4 RGBA grid shaped for upload as an rgba16float 3D texture). Includes a CPU-reference sampler — tetrahedral by default, trilinear fast path — plus an optional 1D shaper applied before the cube and a strength mix toward the original input.

Malformed files fail loudly (validation-error); unsupported-but-well-formed features are rejected as not-implemented rather than silently treated as identity.

Install

pnpm add @faceless-photolib/lut

Usage

import {
  parseLut,
  sampleLut,
  applyLut,
  NO_SHAPER,
} from "@faceless-photolib/lut";

// parseLut returns a Result discriminated union — guard the "ok" branch.
const parsed = parseLut("cube", bytes); // bytes: Uint8Array
if (parsed.kind !== "ok") throw new Error("invalid LUT file");
const lut = parsed.value;

// CPU-reference sample at a normalized RGB coordinate (golden source).
const sampled = sampleLut(lut, [0.5, 0.5, 0.5]);

// Full apply: optional 1D shaper, then the 3D LUT, then a strength mix.
const mapped = applyLut(lut, [0.5, 0.5, 0.5], { shaper: NO_SHAPER, strength: 1 });

API

ExportDescription
parseLut(format, bytes)Parse Uint8Array bytes of a declared LutFormat into Result<Lut3dDescriptor>.
sampleLut(lut, rgb)CPU-reference sample of the LUT at a normalized RGB triple (tetrahedral/trilinear per descriptor).
applyLut(lut, rgb, options)Optional shaper → LUT → blend toward the original input by options.strength (ApplyOptions).
writeCube(lut)Serialize a descriptor back to .cube text (Result<string>).
resolveLut(ref, interpolation)Resolve a LUT by ContentHash; needs an AssetStore injected by the runtime layer.
parseShaperCube(text)Parse a 1D .cube (LUT_1D_SIZE) into a Shaper1d.
applyShaper(shaper, rgb)Apply a per-channel 1D shaper to an RGB triple.
CreateLut3dRequestSchemaZod boundary schema for "create a 3D-LUT layer" requests.
LutFormatSchema / LutInterpolationSchemaZod enums mirroring LutFormat / LutInterpolation.
SUPPORTED_SIZES / DEFAULT_SIZEValidated grid sizes ([17, 33, 65]) and the default (33).
NO_SHAPERThe named "no shaper" slot ({ kind: "none" }).

Key types: Lut3dDescriptor, LutInterpolation ("tetrahedral" | "trilinear"), LutFormat ("cube" | "3dl" | "clf"), ApplyOptions, Shaper1d, NoShaper, ShaperSlot, CreateLut3dRequest.

License

MIT

API reference

21 public exports · 18 documented · generated from source.

applyLutfunction
applyLut(lut: Lut3dDescriptor, rgb: Rgb, options: ApplyOptions): Rgb

Full CPU reference apply: optionally run the 1D shaper, sample the 3D LUT, then blend the mapped result toward the *original* input by `strength` (lut3d spec: strength=0 is identity, strength=1 is the full LUT, intermediate is a proportional linear blend). The strength mix is against the original input, not the post-shaper value, so strength=0 is a true no-op.

applyShaperfunction
applyShaper(shaper: Shaper1d, rgb: Rgb): Rgb

Apply a 1D shaper to an RGB triple.

parseLutfunction
parseLut(format: LutFormat, bytes: Uint8Array<ArrayBufferLike>): Result<Lut3dDescriptor>

Parse raw LUT bytes of a declared format into a descriptor. Each format keeps its own index logic (`.cube` R-fastest float, `.3dl` B-fastest int, `.clf` XML ProcessList B-fastest). A structurally malformed file is a `validation-error`; an unsupported but well-formed feature is `rejected` `not-implemented` with a beacon (never a silent identity).

parseShaperCubefunction
parseShaperCube(text: string): Result<Shaper1d>

Parse a 1D `.cube` (`LUT_1D_SIZE`) into a `Shaper1d`. Each row is a single RGB triple; the same row index is the table index for all three channels.

resolveLutfunction
resolveLut(_ref: string & $brand<"ContentHash">, _interpolation: LutInterpolation): Result<Lut3dDescriptor>

Resolve a LUT referenced by content hash into a descriptor via the asset store. The `lut` leaf package depends only on `schemas`/`color`/ `diagnostics`; the `AssetStore` port that turns a `ContentHash` into bytes lives in a later layer (engine-runtime), so resolution is injected there. Until that wiring exists this fails loudly rather than guessing.

sampleLutfunction
sampleLut(lut: Lut3dDescriptor, rgb: readonly [number, number, number]): readonly [number, number, number]

CPU reference: sample the LUT at a normalized RGB coordinate (golden source).

writeCubefunction
writeCube(lut: Lut3dDescriptor): Result<string>

Serialize a descriptor back to `.cube` text (writer half of the codec).

ApplyOptionsinterface
interface ApplyOptions

Options for the full CPU apply pipeline (shaper + LUT + strength mix).

Lut3dDescriptorinterface
interface Lut3dDescriptor

Backend-agnostic 3D-LUT descriptor (Shader IR node). `data` is the flat float grid (`size³ × 4`, RGBA, R-fastest after normalization). `domainMin`/ `domainMax` are applied as a pre-scale before indexing.

NoShaperinterface
interface NoShaper

Named absence of a shaper.

Shaper1dinterface
interface Shaper1d

Optional 1D shaper / pre-LUT applied per channel before the 3D LUT (design D6: "log/shaper LUT in front of the 3D LUT for scene-linear inputs"). Each channel table is `size` entries over the declared `domainMin..domainMax`. Absence is a named variant, not `undefined` (project.md §4): a layer with no shaper uses `{ kind: "none" }`.

CreateLut3dRequesttype
type CreateLut3dRequest
LutFormattype
type LutFormat

The on-disk LUT formats this package parses.

LutInterpolationtype
type LutInterpolation

Interpolation strategy for sampling a 3D LUT.

ShaperSlottype
type ShaperSlot
CreateLut3dRequestSchemaconst
const CreateLut3dRequestSchema

Boundary request schema for "create a 3D-LUT layer". Validated with `safeParse` at a transport boundary; a failure yields `invalid-request` (project.md §4b) — distinct from a structurally malformed *file*, which is a `validation-error`.

DEFAULT_SIZEconst
DEFAULT_SIZE: 33

Default grid size per design D6 when a format omits a size declaration.

LutFormatSchemaconst
LutFormatSchema: ZodEnum<{ cube: "cube"; "3dl": "3dl"; clf: "clf"; }>

Supported on-disk LUT formats. Mirrors `LutFormat`.

LutInterpolationSchemaconst
LutInterpolationSchema: ZodEnum<{ tetrahedral: "tetrahedral"; trilinear: "trilinear"; }>

Interpolation strategy for sampling a 3D LUT. Mirrors `LutInterpolation`.

NO_SHAPERconst
NO_SHAPER: NoShaper
SUPPORTED_SIZESconst
SUPPORTED_SIZES: readonly number[]

The supported (validated, not silently resampled) cubic grid sizes.

On this page