← All @molecule/* packages · App templates
@molecule/api-image-compositorUtility · image-compositor · API (Node) · v1.0.1 · Apache-2.0
Multi-layer photo composition with masks/blends — extends Sharp with blend-mode + mask compositing.
npm install @molecule/api-image-compositor@molecule/api-image-compositor is a utility package for the API (Node) side (image-compositor).
import { compositeImage } from '@molecule/api-image-compositor'
const png = await compositeImage(
{
width: 1200,
height: 800,
background: '#ffffff',
layers: [
{ kind: 'image', src: backgroundJpg, position: { x: 0, y: 0, width: 1200, height: 800 } },
{
kind: 'gradient',
gradient: {
type: 'linear',
x0: 0,
y0: 0,
x1: 0,
y1: 1,
stops: [
{ offset: 0, color: 'rgba(0,0,0,0)' },
{ offset: 1, color: 'rgba(0,0,0,0.6)' },
],
},
position: { x: 0, y: 0, width: 1200, height: 800 },
blendMode: 'multiply',
},
],
},
{ format: 'png' },
)Works with: @molecule/api-bond, @molecule/api-i18n, @molecule/api-image
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Multi-layer image composition for molecule.dev.
Takes a {@link LayeredImage} document (the server-side analogue of the
shape consumed by @molecule/app-feature-image-canvas-react) and renders a
single flat PNG/JPEG/WebP buffer.
Decoupled from any concrete image library: at runtime the compositor
looks up the bonded @molecule/api-image provider via bond('image')
and uses it for raw-buffer decode/encode/resize. There is no direct
sharp import in handler-callable code.
import { compositeImage } from '@molecule/api-image-compositor'
const png = await compositeImage(
{
width: 1200,
height: 800,
background: '#ffffff',
layers: [
{ kind: 'image', src: backgroundJpg, position: { x: 0, y: 0, width: 1200, height: 800 } },
{
kind: 'gradient',
gradient: {
type: 'linear',
x0: 0,
y0: 0,
x1: 0,
y1: 1,
stops: [
{ offset: 0, color: 'rgba(0,0,0,0)' },
{ offset: 1, color: 'rgba(0,0,0,0.6)' },
],
},
position: { x: 0, y: 0, width: 1200, height: 800 },
blendMode: 'multiply',
},
],
},
{ format: 'png' },
)
// HTTP handler — Express adapter
import { createImageCompositeHandler } from '@molecule/api-image-compositor'
const handle = createImageCompositeHandler()
router.post('/image/composite', async (req, res, next) => {
try {
await handle(
{ body: req.body },
{
setHeader: (n, v) => res.setHeader(n, v),
setStatus: (s) => {
res.status(s)
},
sendBuffer: (b) => {
res.end(b)
},
sendJson: (j) => {
res.json(j)
},
},
)
} catch (err) {
next(err)
}
})
utility
npm install @molecule/api-image-compositor @molecule/api-bond @molecule/api-i18n @molecule/api-image @molecule/api-image-sharp
CompositeOptionsOptions for {@link compositeImage}.
interface CompositeOptions {
/** Output format. Defaults to `'png'`. */
format?: CompositeFormat
/** Output quality (1–100) for lossy formats (`jpeg`, `webp`). Ignored for PNG. */
quality?: number
/** Optional final-resize step applied after compositing. */
resize?: CompositeResize
}
CompositeRequestMinimal request shape consumed by {@link createImageCompositeHandler}.
interface CompositeRequest {
/** Parsed JSON body — `{ doc, options }` envelope. */
body: unknown
}
CompositeResizeOutput dimensions to resize the flattened result to. Both fields are
optional; if omitted, the document's width × height is used as-is.
interface CompositeResize {
/** Target width in pixels. */
width?: number
/** Target height in pixels. */
height?: number
}
CompositeResponseMinimal response shape consumed by {@link createImageCompositeHandler}.
interface CompositeResponse {
/** Set a single response header. */
setHeader: (name: string, value: string) => void
/** Set the HTTP status code. */
setStatus: (status: number) => void
/** Write a binary buffer body and end the response. */
sendBuffer: (buffer: Buffer) => void
/** Write a JSON body and end the response. */
sendJson: (body: unknown) => void
}
CompositorDependenciesDependencies injected into {@link compositeImage}. All fields are
optional; missing entries are resolved via bond('image') at call time.
interface CompositorDependencies {
/** Raster codec for decode/encode/resize. */
raster?: RasterCodec
}
CreateImageCompositeHandlerOptionsOptions for {@link createImageCompositeHandler}.
interface CreateImageCompositeHandlerOptions {
/**
* Optional pre-flight validator. Throw to reject the request; the thrown
* error's `.message` becomes the JSON `error` field with HTTP 400.
*/
validate?: (doc: LayeredImage, options: CompositeOptions) => void | Promise<void>
/** Default suggested filename (without extension). Defaults to `'composite'`. */
filename?: string
/** Optional dependencies forwarded to {@link compositeImage}. */
deps?: CompositorDependencies
}
FillLayerSolid fill layer — paints color over the layer rect.
interface FillLayer extends LayerBase {
kind: 'fill'
/** CSS color string (any form `parseCssColor()` accepts). */
color: string
}
GradientLayerGradient layer — paints a linear or radial gradient over the layer rect.
interface GradientLayer extends LayerBase {
kind: 'gradient'
/** Gradient definition (linear or radial). */
gradient: Gradient
}
GradientStopA single linear / radial gradient stop.
interface GradientStop {
/** Position along the gradient axis in `[0, 1]`. */
offset: number
/** CSS color string for this stop. */
color: string
}
ImageLayerImage layer — src is an encoded image buffer (PNG/JPEG/WebP).
interface ImageLayer extends LayerBase {
kind: 'image'
/** Encoded image buffer to draw. */
src: Buffer
}
LayeredImageThe full layered-document input to {@link compositeImage}.
interface LayeredImage {
/** Document width in pixels. */
width: number
/** Document height in pixels. */
height: number
/** Optional CSS color string for the canvas background. Defaults to transparent. */
background?: string
/** Layers stacked from background → foreground. */
layers: Layer[]
}
LayerMaskA single-channel mask buffer applied to the layer.
Pixel i in data (0..255) scales the corresponding RGBA pixel's alpha
channel by data[i] / 255. The buffer must be exactly width * height
bytes long.
interface LayerMask {
/** Single-channel grayscale mask (one byte per pixel). */
data: Buffer
/** Mask width in pixels. */
width: number
/** Mask height in pixels. */
height: number
}
LayerPositionPixel-space position of a layer relative to the document origin (top-left). Negative values are allowed and the layer is clipped to the document bounds.
interface LayerPosition {
/** Horizontal offset in pixels from the document's left edge. */
x: number
/** Vertical offset in pixels from the document's top edge. */
y: number
/** Optional render width in pixels. Defaults to the source asset width. */
width?: number
/** Optional render height in pixels. Defaults to the source asset height. */
height?: number
}
LinearGradientLinear gradient definition. The gradient axis runs from (x0, y0) to
(x1, y1) in fractional coordinates of the layer rect ([0, 1]).
interface LinearGradient {
type: 'linear'
x0: number
y0: number
x1: number
y1: number
stops: GradientStop[]
}
RadialGradientRadial gradient definition. Center at (cx, cy) with radius r,
all in fractional coordinates of the layer rect ([0, 1]).
interface RadialGradient {
type: 'radial'
cx: number
cy: number
r: number
stops: GradientStop[]
}
RasterCodecMinimal raster contract the compositor needs from the bonded image provider.
The pure-JS layer compositor in {@link compositeRgba} operates on raw
RGBA buffers; this contract supplies the encode/decode round-trip via
the bonded @molecule/api-image provider. Bond providers that do not
implement these methods can be wrapped by passing an explicit
{@link CompositorDependencies.raster}.
interface RasterCodec {
/**
* Decode an encoded image buffer (PNG/JPEG/WebP/etc.) into a raw RGBA
* pixel buffer.
*
* @param buffer - Encoded image bytes.
*/
decode(buffer: Buffer): Promise<RawImage>
/**
* Encode a raw RGBA pixel buffer into the requested image format.
*
* @param raw - Source pixels.
* @param format - Output format.
* @param quality - Optional quality (1–100) for lossy formats.
*/
encode(raw: RawImage, format: CompositeFormat, quality?: number): Promise<Buffer>
/**
* Resize a raw RGBA buffer to the requested dimensions. Implementations
* may choose any sensible interpolation; the pure-JS fallback uses
* nearest-neighbor.
*
* @param raw - Source pixels.
* @param width - Target width.
* @param height - Target height.
*/
resizeRaw(raw: RawImage, width: number, height: number): Promise<RawImage>
}
RawImageRaw RGBA pixel buffer with explicit dimensions. The buffer is always
4 bytes per pixel in R, G, B, A order, row-major top-to-bottom.
interface RawImage {
/** RGBA pixel data (`width * height * 4` bytes). */
data: Buffer
/** Image width in pixels. */
width: number
/** Image height in pixels. */
height: number
}
RgbaRGBA color, each channel in [0, 255].
interface Rgba {
r: number
g: number
b: number
a: number
}
BlendModeLayer blend modes. Implemented as Porter-Duff "source over" by default
('normal'); the rest follow standard SVG / Canvas2D semantics.
Provider-backed compositors may map these onto their native blend mode
names; the pure-JS fallback implements them in applyBlendMode().
type BlendMode = 'normal' | 'multiply' | 'screen' | 'overlay'
CompositeFormatSupported output formats for the flattened image.
type CompositeFormat = 'png' | 'jpeg' | 'webp'
GradientUnion of supported gradient kinds.
type Gradient = LinearGradient | RadialGradient
LayerDiscriminated union of all layer types.
type Layer = ImageLayer | FillLayer | GradientLayer
LayerKindDiscriminated union of layer kinds the compositor understands.
type LayerKind = 'image' | 'fill' | 'gradient'
blendChannel(mode, s, d)Blend the source channel s over the destination channel d per the
named blend mode. All inputs and outputs are 0–255 bytes.
function blendChannel(mode: BlendMode, s: number, d: number): number
mode — Blend mode.s — Source channel (0–255).d — Destination channel (0–255).Returns: Blended channel value.
clamp(value, lo, hi)Clamp a number into [lo, hi].
function clamp(value: number, lo: number, hi: number): number
value — Input value.lo — Lower bound (inclusive).hi — Upper bound (inclusive).composePixel(mode, srcOpacity, dst, dstOffset, src, srcOffset)Compose a single source pixel onto a single destination pixel using the named blend mode + Porter-Duff source-over alpha compositing.
Mutates dst in place at offset dstOffset and reads src at
srcOffset.
function composePixel(
mode: BlendMode,
srcOpacity: number,
dst: Buffer<ArrayBufferLike>,
dstOffset: number,
src: Buffer<ArrayBufferLike>,
srcOffset: number,
): void
mode — Blend mode for the RGB channels.srcOpacity — Per-layer opacity multiplier in [0, 1].dst — Destination buffer.dstOffset — Byte offset within dst.src — Source buffer.srcOffset — Byte offset within src.compositeImage(doc, options, deps)Flatten a layered image document into an encoded image buffer.
Stages:
compositeRgba).raster.resizeRaw).raster.encode).function compositeImage(
doc: LayeredImage,
options?: CompositeOptions,
deps?: CompositorDependencies,
): Promise<Buffer<ArrayBufferLike>>
doc — Layered-image document.options — Output format / quality / optional resize.deps — Optional dependency injection. When omitted, the bonded @molecule/api-image provider is used as the raster codec.Returns: The flattened, encoded image buffer.
compositeRgba(doc, raster)Composite a layered document into a flat RGBA raster, in memory.
function compositeRgba(doc: LayeredImage, raster: RasterCodec): Promise<RawImage>
doc — Layered-image document.raster — Codec used to decode encoded image layer buffers.Returns: The flat RGBA raster sized to the document bounds.
contentTypeForFormat(format)Map an output format to a Content-Type header value.
function contentTypeForFormat(format: CompositeFormat): string
format — Composite output format.createImageCompositeHandler(handlerOptions)Build a (req, res) => Promise<void> handler that invokes
{@link compositeImage} and streams the resulting buffer back.
function createImageCompositeHandler(
handlerOptions?: CreateImageCompositeHandlerOptions,
): (req: CompositeRequest, res: CompositeResponse) => Promise<void>
handlerOptions — Optional validator / filename / deps.Returns: An async handler accepting { body } and a response shim.
extensionForFormat(format)Map an output format to a file extension (without leading dot).
function extensionForFormat(format: CompositeFormat): string
format — Composite output format.getRasterCodec()Resolve the {@link RasterCodec} from the bonded @molecule/api-image
provider.
function getRasterCodec(): RasterCodec
Returns: The bonded provider cast to a raster codec, if it implements the required methods.
isRasterCodec(value)Type guard for {@link RasterCodec}.
function isRasterCodec(value: unknown): boolean
value — Candidate provider.Returns: true when value looks like a {@link RasterCodec}.
lerpRgba(a, b, t)Linearly interpolate two RGBA colors at fraction t in [0, 1].
function lerpRgba(a: Rgba, b: Rgba, t: number): Rgba
a — Start color.b — End color.t — Interpolation fraction.parseCssColor(input)Parse a CSS color string into RGBA bytes.
function parseCssColor(input: string): Rgba | null
input — CSS color string.Returns: Parsed RGBA, or null if the string is unrecognized.
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-image ^1.0.1@molecule/api-image-sharp ^1.0.1@molecule/api-bond@molecule/api-i18n@molecule/api-image@molecule/api-image-sharpWiring: an image bond must be registered before any composite call. At startup:
import { bond } from '@molecule/api-bond'
import { provider } from '@molecule/api-image-sharp'
bond('image', provider)
Without it, compositing throws "No image provider bonded. Call
bond("image", provider) at startup, or pass deps.raster explicitly."
The @molecule/api-* peers (api-bond, api-i18n, api-image) are
peerDependencies — install them in the app; api-image-sharp is an
optional peer, needed only when you wire it as the bonded provider.
Resource intensity: a high-resolution layered document with many blended layers will allocate sizeable RGBA buffers (4 bytes per pixel, per layer at peak). For flagship apps with unrestricted user input, gate the handler behind a queue or rate limiter rather than calling it inline on the hot request path.