layerforge
v0.2.0
Published
Deterministic headless compositor for layer-manifest product previews.
Maintainers
Readme
layerforge
Deterministic preview compositor for layered product images. Give it a JSON manifest and receive AVIF and WebP files for cards, detail views, and zoom.
Features
- Composites up to 128 layers with masks, blend modes, offsets, scale, and rotation.
- Fills a masked layer with a material color, or tiles an uploaded material texture.
- Optional texture tiling and relief displacement through seamless-texture.
- Shape plugins reshape a layer before texturing. The package ships no product profiles.
- Resizes one lossless master into six artifacts: card (320), detail (800), and zoom (1600), each in AVIF and WebP.
- Runs as a library, a CLI, or an HTTP service with one serialized queue and a subprocess worker.
Install
npm install layerforgeNode.js 22 or later is required.
Library
import { renderLayerArtifactSet, renderLayerManifest } from "layerforge";
const master = await renderLayerManifest(manifest, { assetOrigin: "https://shop.example" });
const artifacts = await renderLayerArtifactSet(manifest, {
assetOrigin: "https://shop.example",
shapes,
onTiming: (event) => console.log(event),
});renderLayerManifest returns a lossless WebP master. renderLayerArtifactSet returns the six responsive files.
Manifest
{
"version": 2,
"kind": "layers_v2",
"revision_id": "10000000-0000-4000-8000-000000000001",
"output": { "width": 1200, "height": 900, "background": "#F7F2E8" },
"layers": [
{
"id": "20000000-0000-4000-8000-000000000001",
"layer_key": "base",
"role": "base",
"path": "/media/base.png",
"content_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"mask_path": null,
"mask_content_hash": null,
"material_texture_path": null,
"material_texture_content_hash": null,
"blend_mode": "normal",
"opacity_basis_points": 10000,
"offset_x_micropixels": 0,
"offset_y_micropixels": 0,
"scale_x_millionths": 1000000,
"scale_y_millionths": 1000000,
"rotation_millidegrees": 0,
"z_order": 10
}
]
}| Layer field | Meaning |
| --- | --- |
| id, layer_key, role | Identity. IDs and keys must be unique. |
| path, content_hash | Source image. The SHA-256 hash is a 64-character hex string. |
| blend_mode | normal, multiply, screen, overlay, or destination_in. |
| mask_path, mask_content_hash | Optional alpha mask, resized to the layer. Set both to null when absent. |
| material_color_hex | Optional solid fill through the source alpha. |
| material_texture_path, material_texture_content_hash | Optional tiled texture. Set both to null when absent. |
| material_texture_surface | Apply seamless tiling and relief displacement. |
| material_texture_scale_millionths | Texture scale, 100000 to 8000000. |
| shape_profile, shape_width_mm | Optional shape plugin and its width. |
| opacity_basis_points | 0 to 10000, where 10000 is opaque. |
| offset_x_micropixels, offset_y_micropixels | Center offset in millionths of a pixel. |
| scale_x_millionths, scale_y_millionths | Layer scale in millionths. |
| rotation_millidegrees | Rotation in thousandths of a degree. |
| z_order | Draw order. Ties sort by layer_key, then id. |
Asset paths must stay under the asset prefixes (/media/ and /cad/ by default) and must have no query, fragment, or .. segment. Set assetPrefixes to change the allowed prefixes.
Validation
validateLayerManifest checks a manifest without rendering and reports every problem. Each error has a JSON-pointer-style path and a message:
import { validateLayerManifest } from "layerforge";
const { valid, errors } = validateLayerManifest(manifest);
for (const { path, message } of errors) {
console.error(`${path}: ${message}`);
}normalizeLayerManifest applies the same checks and returns the normalized manifest with the layers sorted by z_order. It throws an error with one line per problem when the manifest is invalid. A render validates the manifest first, so an invalid manifest fails with the same errors before any asset is loaded.
The CLI checks a file without rendering:
layerforge --validate manifest.jsonIt prints one path: message line per error. The exit code is 0 for a valid manifest, 1 for an invalid manifest, and 2 for a usage error.
schema/layer-manifest.schema.json is a JSON Schema draft 2020-12 for the manifest. Editors pick it up through the $schema property:
{
"$schema": "./node_modules/layerforge/schema/layer-manifest.schema.json",
"version": 2,
"kind": "layers_v2"
}The published URL is https://raw.githubusercontent.com/M1chlCZ/layerforge/main/schema/layer-manifest.schema.json. The schema covers the default asset prefixes (/media/ and /cad/). validateLayerManifest also checks the rules JSON Schema cannot express: unique layer id and layer_key values, the output pixel budget, and custom assetPrefixes. Unknown properties are accepted at every level; the renderer ignores top-level extras and passes layer extras to shape plugins.
Shape plugins
A shape plugin reshapes or recolors one layer before texturing:
export const shapes = {
rounded: async (source, { widthMm, layer }) => sharp(source)
.resize({ width: widthMm * 10, fit: "fill" })
.png()
.toBuffer(),
};Pass the module to the library through shapes, to the CLI through --shapes FILE, or to the service through SHAPES_MODULE. A layer with an unknown profile fails the render. A layer with a shape profile skips the material color fill, because the plugin owns the look of the layer.
CLI
layerforge render manifest.json --out preview --asset-origin https://shop.example --shapes shapes.mjs
layerforge --validate manifest.json
layerforge serve --port 3100 --asset-origin https://shop.exampleThe CLI reads ASSET_ORIGIN, SHAPES_MODULE, and PORT when the flag is absent.
Service
POST /renderwith the manifest as JSON returnsmultipart/mixedwith the six artifacts andX-Preview-Role,X-Preview-Width, andX-Preview-Heightheaders.GET /healthreturns{"status":"ok"}.- One render runs at a time. Each render runs in a subprocess with a 65 second limit.
- The asset origin is trusted. Content hashes are format-checked, so configure an origin that you control.
docker build -t layerforge .
docker run --rm -p 3100:3100 -e ASSET_ORIGIN=https://shop.example layerforgeLimits
| Item | Limit | | --- | --- | | Output | 6000 per axis, 16 megapixels | | Layers | 128 | | Asset | 16 MiB | | Manifest | 1 MiB |
Third-party licenses
sharpand its@img/sharp-*platform packages use Apache-2.0.- The
@img/sharp-libvips-*platform packages use LGPL-3.0-or-later. npm installs them for the target platform; they are not part of this package. - Keep the license texts when you redistribute a built image or a bundled copy.
Development
npm ci
npm test
npm run benchmarkLicense
MIT. See LICENSE.
