faceless-photolib

@faceless-photolib/schemas

Zod schemas as the single source of truth for faceless-photolib types — branded IDs, the Result/Resource vocabulary, the Layer discriminated union, color spaces, transforms, and masks.

Zod schemas as the single source of truth for faceless-photolib — a headless, color-managed, GPU-accelerated image-editing engine.

Every cross-package type — branded IDs, the Result / Resource vocabulary, the Layer discriminated union, color-space identifiers, transforms, and masks — is defined once here as a Zod schema with its inferred TypeScript type. There are no optional or nullable fields; unions are named and discriminated, so invalid states fail safeParse rather than slipping through.

Install

pnpm add @faceless-photolib/schemas

Usage

import { match } from "ts-pattern";
import {
  LayerIdSchema,
  LayerSchema,
  ok,
  rejected,
  type Result,
  resource,
  ready,
  type Layer,
} from "@faceless-photolib/schemas";

// Branded IDs: a LayerId is not assignable from a bare string.
const id = LayerIdSchema.parse("layer-1");

// Validate untrusted input against the Layer discriminated union.
const parsed = LayerSchema.safeParse(input);
if (!parsed.success) {
  // parsed.error.issues describes exactly what was wrong.
}

// Construct canonical results instead of throwing or returning null.
function lockedEdit(layer: Layer): Result<Layer> {
  if (layer.locks.composite) return rejected("locked", "composite lock is active");
  return ok(layer);
}

// Pattern-match the result; the union forces every variant to be handled.
const message = match(lockedEdit(layer))
  .with({ kind: "ok" }, () => "applied")
  .with({ kind: "rejected" }, (r) => `rejected: ${r.reason} — ${r.detail}`)
  .otherwise(() => "other outcome");

// Resource<T> models async/heavy work with explicit idle/loading/ready/error states.
const renderSchema = resource(LayerSchema);
const state = ready(layer); // { status: "ready", value: layer }

API

ExportDescription
LayerIdSchema, DocumentIdSchema, AssetIdSchema, MaskIdSchema, ContentHashSchemaBranded string-ID schemas (with matching inferred types); structurally incompatible with each other.
LayerSchema / LayerDiscriminated union (on type) of image, adjustment, lut3d, colorTransform, fill, text, and recursive group layers.
result(value) / type Result<T>Result-vocabulary factory and TS union (ok, not-found, invalid-request, validation-error, rejected, conflict, backend-unavailable).
ok, notFound, invalidRequest, validationError, rejected, conflict, backendUnavailableVariant constructors for Result<T>.
issuesFromZod(error)Maps a ZodError's issues into the canonical Issue[] shape.
resource(value) / type Resource<T>Async-lifecycle factory and TS union with idle, loading, ready, error.
idle, loading, ready, erroredVariant constructors for Resource<T>.
ColorSpaceSchema, ColorSpaceIdSchema, PrimariesSchema, TransferFnSchema, ChromaticitySchemaColor-management schemas (ACEScg-centric color spaces, primaries, transfer functions).
BlendModeSchema, BlendSpaceSchemaBlend-mode and blend-space enums.
TransformSchema, Mat3Schema, DecomposedTransformSchema, ResampleQualitySchema, IDENTITY_MAT3Geometry: affine transforms, 3×3 matrices, resample quality.
MaskSchema / Mask, NO_MASKLayer mask schema plus the canonical empty-mask constant.
DocumentSchema, CanvasSizeSchema, DocumentSettingsSchema, CURRENT_SCHEMA_VERSIONDocument model and current schema version.
StoredDocumentSchema, DocV1SchemaOn-disk file-format schemas.
RenderResultSchema, ViewportSchema, SurfaceSchemaRender-output, viewport, and surface schemas.

Each schema is paired with its z.infer TypeScript type (e.g. LayerSchema / Layer). engine-api re-exports the Result / Resource vocabulary so leaf packages can construct outcomes without depending on it.

License

MIT

API reference

113 public exports · 58 documented · generated from source.

backendUnavailablefunction
backendUnavailable<T = never>(detail: string): Result<T>
collectDocV1AssetHashesfunction
collectDocV1AssetHashes(doc: { [x: string]: unknown; schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly LayerV1[]; }): readonly (string & $brand<...>)[]

Collect the distinct bare asset hashes a v1 document's image layers reference (groups walked recursively), in first-appearance order. The engine resolves these into `ContentRef`s before calling the migration.

computeHistogramsfunction
computeHistograms(result: { width: number; height: number; colorSpace: string & $brand<"ColorSpaceId">; pixels: Float32Array<ArrayBuffer>; }): { r: Uint32Array<ArrayBuffer>; g: Uint32Array<...>; b: Uint32Array<...>; luma: Uint32Array<...>; peak: number; }

Per-channel 256-bin histograms (R, G, B, luma) from straight f32 RGBA pixels (stride 4). This is the CPU golden reference the GPU compute histogram pass is conformance-checked against (gpu-histogram spec, "CPU-reference conformance"), and it backs the Histogram panel and features-page demo (via the injected CPU test backend's pixel-bearing `RenderResult`). `peak` is the max R/G/B bin count only (see {@link peakOfChannels}).

conflictfunction
conflict<T = never>(what: string, detail: string): Result<T>
erroredfunction
errored<T = never>(message: string): Resource<T>
idlefunction
idle<T = never>(): Resource<T>
imageSourceKeyfunction
imageSourceKey(source: { t: "content"; ref: { algo: "b3"; digest: string & $brand<"HexDigest">; mediaType: string & $brand<"MediaType">; role: "master" | "preview" | "cache" | "document" | "history"; bytes: number; }; } | { ...; }): string

The stable string key a source lowers to for render-graph pass identity and synchronous `SourceResolver` lookups: a content source keys by its versioned `v1:b3:<hex>` ref string, an external source by its URI. Two sources with the same key are the same pixels.

invalidRequestfunction
invalidRequest<T = never>(issues: readonly { path: readonly (string | number)[]; message: string; code: string; }[]): Result<T>
issuesFromZodfunction
issuesFromZod(error: ZodError<unknown>): readonly { path: readonly (string | number)[]; message: string; code: string; }[]

Convert a `ZodError`'s issues into the canonical `Issue[]` shape.

loadingfunction
loading<T = never>(): Resource<T>
migrateDocV1toV2function
migrateDocV1toV2(doc: { [x: string]: unknown; schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly LayerV1[]; }, assetRefs: V1AssetRefIndex): Result<...>

The first REAL step of the migration chain: `DocV1 → DocV2` (native-file-format D7). Pure and deterministic — a function of the parsed v1 document and the caller-supplied asset-ref index only; migrating the same inputs twice yields byte-identical v2 documents (and identical records). Unknown top-level forward-compatible fields are preserved; the original v1 state is untouched (the record's `sourceRef` is its canonical content ref, so the pre-migration bytes remain retrievable wherever they were stored). Failure modes (all `validation-error`, naming the offending fields; no partially-migrated document is ever returned): - an image asset hash the index cannot map, - a produced payload that fails `DocV2Schema` (the "broken step" contract of the file-format spec), - a document that cannot be canonicalized for the provenance record.

notFoundfunction
notFound<T = never>(what: string): Result<T>
okfunction
ok<T>(value: T): Result<T>
peakOfChannelsfunction
peakOfChannels(r: Uint32Array<ArrayBufferLike>, g: Uint32Array<ArrayBufferLike>, b: Uint32Array<ArrayBufferLike>): number

The max R/G/B bin count across all 256 bins — the value both histogram UIs (`HistogramPanel.tsx`, features-page demo) scale their bar heights against, shared here so neither duplicates the reduction. Luma is deliberately excluded because it is every channel's weighted sum and typically taller than any single channel; folding it in would shrink the R/G/B bars.

readyfunction
ready<T>(value: T): Resource<T>
rejectedfunction
rejected<T = never>(reason: "not-implemented" | "locked" | "degenerate" | "unsupported" | "out-of-range" | "conflict", detail: string): Result<T>
resourcefunction
resource<T extends z.ZodTypeAny>(value: T): ZodDiscriminatedUnion<[ZodObject<{ status: ZodLiteral<"idle">; }, $strip>, ZodObject<{ status: ZodLiteral<"loading">; }, $strip>, ZodObject<...>, ZodObject<...>], "status">

`Resource<T>` — the lifecycle of asynchronous or heavy work (a render, an export). Held with `@observable.ref` in UI samples; every async UI action renders all four states distinctly (project.md §4, engine-api spec). Note the controller `idle` *lifecycle* state (interaction-controllers spec) is a different concept from this async `idle` — controllers are synchronous and have no `loading` phase.

resultfunction
result<T extends z.ZodTypeAny>(value: T): ZodDiscriminatedUnion<[ZodObject<{ kind: ZodLiteral<"ok">; value: T; }, $strip>, ZodObject<{ kind: ZodLiteral<"not-found">; what: ZodString; }, $strip>, ... 4 more ..., ZodObject<...>], "kind">

Generic result factory. `result(z.object({...}))` yields a Zod schema for the success payload plus every canonical failure variant; `z.infer` of it is the `Result<T>` TS type. Discriminated on `kind`, so callers `match(result).with({ kind: "ok" }, …).exhaustive()`.

upgradeStoredDocumentfunction
upgradeStoredDocument(stored: { [x: string]: unknown; schemaVersion: 1; id: string & $brand<"DocumentId">; name: string; size: { width: number; height: number; }; settings: { connectionSpace: string & $brand<"ColorSpaceId">; blendSpace: "linear-light" | "perceptual"; outputTransform: "aces-1.x" | "aces-2.0"; dissolveSeed: number; }; layers: readonly LayerV1[]; } | { ...; }, ctx: { ...; }): Result<...>

Apply the migration chain to a parsed stored document until it reaches the current version (file-format spec: deterministic linear chain). A current- version document passes through untouched with no records; an older document is upgraded step by step, each step contributing its provenance record. Any failing step surfaces its `validation-error` unchanged — never a partially migrated document.

validationErrorfunction
validationError<T = never>(fields: readonly { path: readonly (string | number)[]; message: string; code: string; }[]): Result<T>
MigratedDocV2interface
interface MigratedDocV2

A successful migration step's output: the upgraded doc + its provenance record.

UpgradedDocumentinterface
interface UpgradedDocument

The outcome of upgrading a stored document to the current version.

AdjustmentParamstype
type AdjustmentParams
AssetIdtype
type AssetId
BlendModetype
type BlendMode
BlendSpacetype
type BlendSpace
CanvasSizetype
type CanvasSize
Chromaticitytype
type Chromaticity
ColorSpacetype
type ColorSpace
ColorSpaceIdtype
type ColorSpaceId
ContentHashtype
type ContentHash
DecomposedTransformtype
type DecomposedTransform
Documenttype
type Document
DocumentIdtype
type DocumentId
DocumentSettingstype
type DocumentSettings
DocV1type
type DocV1
DocV2type
type DocV2
FillContenttype
type FillContent
GroupIsolationtype
type GroupIsolation
GroupLayertype
type GroupLayer

A group layer, expressed explicitly because of the recursive `children` field.

GroupLayerV1type
type GroupLayerV1

A v1 group layer, expressed explicitly because of the recursive `children`.

HistogramStatetype
type HistogramState
HistogramUnavailableReasontype
type HistogramUnavailableReason
ImageSourcetype
type ImageSource
Issuetype
type Issue
Layertype
type Layer

`Layer` — a discriminated union on `type` with a shared base (document-model spec; D4). `group` is recursive (children are themselves `Layer`s) so groups nest to arbitrary depth, hence the `z.lazy` for the child stack. An unknown `type`, missing discriminant, or mismatched variant-field is unrepresentable: it fails `safeParse`.

LayerIdtype
type LayerId
LayerLockstype
type LayerLocks
LayerTypetype
type LayerType
LayerV1type
type LayerV1

The v1 layer union (recursive through `group.children`).

Masktype
type Mask
MaskIdtype
type MaskId
Mat3type
type Mat3
MigrationRecordtype
type MigrationRecord
MigrationSteptype
type MigrationStep
OutputTransformVersiontype
type OutputTransformVersion
Primariestype
type Primaries
RejectReasontype
type RejectReason
RenderResulttype
type RenderResult
ResampleQualitytype
type ResampleQuality
Resourcetype
type Resource

`Resource<T>` as a plain TS discriminated union.

Resulttype
type Result

`Result<T>` as a plain TS discriminated union, decoupled from any specific payload schema so signatures across the workspace can name `Result<Foo>` uniformly.

StoredDocumenttype
type StoredDocument
Surfacetype
type Surface
TransferFntype
type TransferFn
Transformtype
type Transform
V1AssetRefIndextype
type V1AssetRefIndex

The data a v1 → v2 migration needs beyond the document itself: for every v1 bare asset hash, the full typed `ContentRef` the rewritten `{ t: "content" }` source will carry. Plain data (hash → ref) so the migration stays a pure, deterministic function of its inputs — the CALLER (engine-api) performs the I/O of resolving bytes and minting refs, then hands the index in.

Viewporttype
type Viewport
AdjustmentParamsSchemaconst
AdjustmentParamsSchema: ZodObject<{ effect: ZodString; params: ZodRecord<ZodString, ZodUnknown>; }, $strip>

Adjustment payloads (interpreted by the `adjustments` package).

AssetIdSchemaconst
AssetIdSchema: $ZodBranded<ZodString, "AssetId", "out">
BlendModeSchemaconst
const BlendModeSchema

The complete set of 27 blend modes (blend-compositing spec; D5). Order mirrors the spec's enumeration: 12 separable, then the Photoshop-only modes, then the 4 non-separable HSL modes. A value outside this set fails `safeParse` and returns `invalid-request` — never silently substituted with `normal`. Names are the camelCase identifiers used throughout the engine; the spec's display names map 1:1 (e.g. "Linear Dodge (Add)" → `linearDodge`).

BlendSpaceSchemaconst
BlendSpaceSchema: ZodEnum<{ "linear-light": "linear-light"; perceptual: "perceptual"; }>

Per-document toggle selecting the space blends are computed in (blend-compositing spec; D5): physically-correct linear-light vs Photoshop-parity perceptual.

CanvasSizeSchemaconst
CanvasSizeSchema: ZodObject<{ width: ZodNumber; height: ZodNumber; }, $strip>

Pixel dimensions of the document canvas.

ChromaticitySchemaconst
ChromaticitySchema: ZodObject<{ x: ZodNumber; y: ZodNumber; }, $strip>

A CIE xy chromaticity coordinate.

ColorSpaceIdSchemaconst
ColorSpaceIdSchema: $ZodBranded<ZodString, "ColorSpaceId", "out">

A color-space id, branded so an unknown id cannot masquerade as a resolved one.

ColorSpaceSchemaconst
const ColorSpaceSchema

A registered color space. `toReference`/`fromReference` are 3x3 row-major matrices.

ContentHashSchemaconst
ContentHashSchema: $ZodBranded<ZodString, "ContentHash", "out">

A content-addressed asset hash (CAS). Assets are referenced by hash, never inlined.

CURRENT_SCHEMA_VERSIONconst
CURRENT_SCHEMA_VERSION: 2

The current persisted document schema version literal (v2: content-ref image sources).

DecomposedTransformSchemaconst
const DecomposedTransformSchema

Decomposed transform components (W3C "unmatrix"). Transforms are *stored* decomposed so interactive handles stay editable and recomposed to a `Mat3` at render (transform-geometry spec; D8). This is a required named field of every `Transform`, never optional.

DOC_MIGRATION_CHAINconst
DOC_MIGRATION_CHAIN: readonly { id: string; from: number; to: number; }[]

The declared chain: each shipped version `vN` has exactly one `up()` step to `vN+1`. {@link upgradeStoredDocument} applies the steps in order.

DOC_MIGRATION_V1_TO_V2const
DOC_MIGRATION_V1_TO_V2: "doc-migration/v1-to-v2@1"

The stable identifier of the v1 → v2 migration. Recorded in every {@link MigrationRecord} so provenance survives as data — a package that carries `(sourceRef, migrationId) → resultRef` can always retrieve the pre-migration bytes and name the transform that produced the current state.

DocumentIdSchemaconst
DocumentIdSchema: $ZodBranded<ZodString, "DocumentId", "out">
DocumentSchemaconst
const DocumentSchema

The in-memory `Document` model. Layers are an ordered stack — index 0 is the bottom-most, the last index is the top-most (document-model spec). Every edit produces a new `Document` value (immer); source pixels are never mutated.

DocumentSettingsSchemaconst
const DocumentSettingsSchema

Document-level rendering settings (color-management + blend specs). `connectionSpace` is the working/reference space (ACEScg in v1); the blend space toggle and output-transform version are explicit, never inferred.

DocV1Schemaconst
const DocV1Schema

The persisted v1 document (file-format spec; D10). `.passthrough()` preserves unknown forward-compatible fields so a load → no-op → save round-trip is byte-stable (the engine MUST NOT drop content it did not intentionally change).

DocV2Schemaconst
const DocV2Schema

v2 (current): image layers reference assets through the typed `{ t: "content", ref } | { t: "external", uri }` source union — never a bare hash string (native-file-format D7).

FillContentSchemaconst
const FillContentSchema

Fill content as a named union — solid, linear gradient, or radial gradient.

GroupIsolationSchemaconst
GroupIsolationSchema: ZodEnum<{ isolated: "isolated"; "pass-through": "pass-through"; }>

Group isolation mode — an explicit field; the engine never infers it.

HISTOGRAM_BINSconst
HISTOGRAM_BINS: 256

Per-channel bin count: straight f32 values quantize to 256 8-bit levels.

HistogramStateSchemaconst
const HistogramStateSchema
HistogramUnavailableReasonSchemaconst
HistogramUnavailableReasonSchema: ZodLiteral<"compute-unsupported">

The one currently-named reason a device's histogram compute path is unavailable (D5).

IDENTITY_MAT3const
IDENTITY_MAT3: [number, number, number, number, number, number, number, number, number]

The 3x3 identity, row-major.

ImageSourceSchemaconst
const ImageSourceSchema

Image-layer asset sources (native-file-format D7). A source is a discriminated union — never a bare hash string: - `{ t: "content", ref }` — the asset lives in a content-addressed store and is referenced by the typed, versioned `ContentRef` (`v1:b3:<hex>` string form with `algo`/`digest`/`mediaType`/`role`/`bytes` metadata). - `{ t: "external", uri }` — the asset is NOT managed by the content store; the URI is an opaque resolver key (a URL, a host-registered upload id, …). A persisted document carrying a bare unversioned hash where a source is expected fails `safeParse` with an issue path naming the field — the engine surfaces it as `validation-error` and constructs no document.

IssueSchemaconst
const IssueSchema

One structured validation finding (mirrors a Zod issue without the union sprawl).

LayerIdSchemaconst
LayerIdSchema: $ZodBranded<ZodString, "LayerId", "out">

Branded string IDs. Branding makes a `LayerId` structurally incompatible with a `DocumentId` even though both are strings — you cannot pass one where the other is expected (project.md §4: brand IDs).

LayerLocksSchemaconst
LayerLocksSchema: ZodObject<{ transparency: ZodBoolean; composite: ZodBoolean; position: ZodBoolean; }, $strip>

Layer locks (document-model spec; D4). Each is an explicit boolean — an operation that violates an active lock returns `rejected` naming the lock, never silently applies or silently ignores.

LayerSchemaconst
LayerSchema: ZodType<Layer, unknown, $ZodTypeInternals<Layer, unknown>>
MaskIdSchemaconst
MaskIdSchema: $ZodBranded<ZodString, "MaskId", "out">
MaskSchemaconst
const MaskSchema

A layer mask as a discriminated union. The `none` variant means "no mask" — never `null`/`undefined` (document-model spec: "absent mask is a named variant, not null"). v1 supports raster masks; the raster bytes are referenced by content hash through the asset store.

Mat3Schemaconst
Mat3Schema: ZodTuple<[ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber, ZodNumber], null>

`Mat3` — a 3x3 row-major matrix as a 9-tuple. Affine = last row [0,0,1]; perspective = full homography. Coordinate convention is fixed: top-left origin, Y-down (transform-geometry spec; D8). No backend may flip it.

MigrationRecordSchemaconst
const MigrationRecordSchema

Migration linkage recorded as data (native-file-format D7): the canonical (JCS + BLAKE3) ref of the source document, the migration id, and the canonical ref of the result document. Deterministic: the same source doc and the same asset index always produce the same record.

MigrationStepSchemaconst
MigrationStepSchema: ZodObject<{ id: ZodString; from: ZodNumber; to: ZodNumber; }, $strip>

One step of the linear migration chain, as data (id + version endpoints).

NO_MASKconst
const NO_MASK

The "no mask" sentinel value, reused everywhere a default mask is needed.

OutputTransformVersionSchemaconst
OutputTransformVersionSchema: ZodEnum<{ "aces-1.x": "aces-1.x"; "aces-2.0": "aces-2.0"; }>

Selects the output-transform generation; ACES 1.x is the default (color-management spec).

PrimariesSchemaconst
const PrimariesSchema

RGB primaries + white point as CIE xy coordinates.

RejectReasonSchemaconst
const RejectReasonSchema

Business-rejection reasons. `not-implemented` is always paired with a beacon.

RenderResultSchemaconst
const RenderResultSchema

`RenderResult` — pure pixel data produced by a render (the payload of a `Resource<RenderResult>`). Lives in `schemas` because it carries no effect-IR references, so every consumer (render-graph, backends, engine-api, color-conformance) can name `Resource<RenderResult>` without extra coupling. Pixels are f32 RGBA in the named color space; integer quantization only ever happens at the export boundary, never here (color-management spec).

ResampleQualitySchemaconst
ResampleQualitySchema: ZodEnum<{ bilinear: "bilinear"; "mipmap-trilinear": "mipmap-trilinear"; bicubic: "bicubic"; lanczos: "lanczos"; }>

Resampling quality for a transformed layer (transform-geometry spec; D8).

StoredDocumentSchemaconst
const StoredDocumentSchema

The versioned union. Add `DocV3Schema`, … here as new versions ship.

SurfaceSchemaconst
SurfaceSchema: ZodEnum<{ web: "web"; expo: "expo"; server: "server"; mcp: "mcp"; "node-sdk": "node-sdk"; }>

The transports/runtimes the one engine drives identically (project.md §6). `mcp` is a future surface but kept in the union, never dropped.

TransferFnSchemaconst
const TransferFnSchema

Transfer function as a discriminated union. `linear` is the connection-space EOTF; named curves carry their parameters explicitly. Unknown curves are a `not-implemented` rejection at resolve time, never a silent identity.

TransformSchemaconst
const TransformSchema

A transform is a discriminated union on `kind`. Both variants carry the recomposed `matrix` and the canonical `decomposed` components (a named field, not an optional) so storage keeps handles editable while render reads the matrix directly.

ViewportSchemaconst
ViewportSchema: ZodObject<{ x: ZodNumber; y: ZodNumber; width: ZodNumber; height: ZodNumber; scale: ZodNumber; }, $strip>

A rectangular region of the canvas to render (top-left origin, Y-down).