Apply custom LUT (Look-Up Table) filters to achieve brand-consistent color grading in server-side applications using CE.SDK’s headless Creative Engine.
LUT filters remap colors through a predefined transformation table, enabling cinematic color grading and consistent brand aesthetics. This guide shows how to apply your own LUT files directly to design elements using the effect API in server-side Node.js applications.
Understanding LUT Image Format#
CE.SDK uses a tiled PNG format where a 3D color cube is laid out as a 2D grid. Each tile represents a slice of the color cube along the blue axis.
The LUT image requires two configuration values:
horizontalTileCount- Number of tiles across the image widthverticalTileCount- Number of tiles down the image height
CE.SDK supports these tile configurations:
- 5×5 tiles with 128px cube size
- 8×8 tiles with 512px cube size
Standard .cube files must be converted to this tiled PNG format using image processing tools.
Creating LUT PNG Images#
Obtaining LUT Files#
LUT files are available from multiple sources:
- Color grading software - Adobe Photoshop, DaVinci Resolve, and Affinity Photo can export 3D LUT files in
.cubeformat - Online LUT libraries - Many free and commercial LUT packs are available for download
- LUT generators - Tools that create custom color transformations from reference images
Converting .cube to Tiled PNG#
CE.SDK requires LUTs in a specific tiled PNG format where each tile represents a slice of the 3D color cube along the blue axis. To convert a standard .cube file:
- Parse the .cube file - Read the 3D color lookup table data
- Arrange slices as tiles - Each blue channel value becomes a separate tile containing the red-green color plane
- Export as PNG - Save the grid as a PNG image
CE.SDK’s built-in LUTs follow a naming convention: imgly_lut_{name}_{h}_{v}_{cubeSize}.png where h and v are tile counts and cubeSize indicates the LUT precision.
Using Python for Conversion#
You can write a Python script using PIL/Pillow and NumPy to convert .cube files:
# Pseudocode for .cube to tiled PNG conversion# 1. Parse the .cube file to extract the 3D LUT data# 2. Reshape data into (blue_slices, height, width, 3) array# 3. Arrange slices in a grid matching tile configuration# 4. Save as PNG with Image.fromarray()Using CE.SDK’s Built-in LUTs#
The simplest approach is to use CE.SDK’s existing LUT assets as a starting point. The built-in filters use pre-generated tiled PNGs that you can reference for format verification. Check the filter extension at ly.img.cesdk.filters.lut for examples of properly formatted LUT images.
Initialize the Engine#
We start by initializing the headless Creative Engine with a scene and page for our image processing workflow.
// Initialize the headless Creative Engineconst engine = await CreativeEngine.init({ // license: process.env.CESDK_LICENSE});
try { // Create a scene with a page engine.scene.create('VerticalStack', { page: { size: { width: 800, height: 600 } } });
const page = engine.block.findByType('page')[0];Add an Image Block#
Add an image block to apply the LUT filter. The addImage() convenience API simplifies image block creation.
// Add an image block to apply the LUT filterconst imageUri = 'https://img.ly/static/ubq_samples/sample_1.jpg';const imageBlock = await engine.block.addImage(imageUri, { x: 150, y: 100, size: { width: 500, height: 400 }});engine.block.appendChild(page, imageBlock);Create the LUT Effect#
Create a lut_filter effect instance using the effect API.
// Create a LUT filter effectconst lutEffect = engine.block.createEffect('//ly.img.ubq/effect/lut_filter');This creates an effect that can be configured and applied to image blocks.
Configure LUT Properties#
Set the LUT file URL and tile dimensions to match your LUT image.
// Configure the LUT file URI - this is a tiled PNG containing the color lookup tableconst lutUrl = 'https://cdn.img.ly/packages/imgly/cesdk-js/1.67.0/assets/extensions/ly.img.cesdk.filters.lut/LUTs/imgly_lut_ad1920_5_5_128.png';engine.block.setString(lutEffect, 'effect/lut_filter/lutFileURI', lutUrl);
// Set the tile grid dimensions - must match the LUT image structureengine.block.setInt(lutEffect, 'effect/lut_filter/horizontalTileCount', 5);engine.block.setInt(lutEffect, 'effect/lut_filter/verticalTileCount', 5);The tile counts must match the actual LUT image grid structure. Using incorrect values produces distorted colors.
Set Filter Intensity#
Control the strength of the color transformation with intensity.
// Set filter intensity (0.0 = no effect, 1.0 = full effect)engine.block.setFloat(lutEffect, 'effect/lut_filter/intensity', 0.8);Values range from 0.0 (no effect) to 1.0 (full effect). Use intermediate values for subtle color grading.
Apply the Effect#
Attach the configured effect to an image block.
// Apply the effect to the image blockengine.block.appendEffect(imageBlock, lutEffect);The effect renders immediately after being applied.
Toggle the Effect#
Enable or disable the effect without removing it.
// Toggle the effect off and back onengine.block.setEffectEnabled(lutEffect, false);const isEnabled = engine.block.isEffectEnabled(lutEffect);console.log('Effect enabled:', isEnabled); // false
engine.block.setEffectEnabled(lutEffect, true);Disabling preserves all effect settings while temporarily removing the visual transformation.
Manage Effects#
Retrieve and inspect effects applied to a block.
// Retrieve all effects on the blockconst effects = engine.block.getEffects(imageBlock);console.log('Number of effects:', effects.length); // 1
// Check if block supports effectsconst supportsEffects = engine.block.supportsEffects(imageBlock);console.log('Supports effects:', supportsEffects); // trueUse getEffects() to access all effects on a block and supportsEffects() to verify compatibility before applying.
Export and Cleanup#
After applying the LUT filter, we export the result to a file and dispose of the engine to free resources.
// Export the result to a fileconst outputDir = './output';if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true });}
const blob = await engine.block.export(page, { mimeType: 'image/png' });const buffer = Buffer.from(await blob.arrayBuffer());writeFileSync(`${outputDir}/custom-lut-filter.png`, buffer);
console.log('Exported result to output/custom-lut-filter.png');Always dispose of the engine in a finally block to ensure resources are freed even if an error occurs.
// Always dispose of the engine to free resourcesengine.dispose();Troubleshooting#
LUT Not Rendering#
- Verify the LUT image URL is accessible from your server
- Confirm the image uses PNG format
- Check that tile count values match the actual image grid
Colors Look Wrong#
- Verify tile counts match the LUT image structure
- Ensure the LUT was generated with sRGB color space
Effect Not Visible in Export#
- Verify the effect is enabled with
isEffectEnabled() - Ensure the effect was appended to the block, not just created
- Check the block supports effects using
supportsEffects()
API Reference#
| Method | Description |
|---|---|
engine.block.createEffect('//ly.img.ubq/effect/lut_filter') | Create a LUT filter effect instance |
engine.block.setString(effect, 'effect/lut_filter/lutFileURI', uri) | Set the LUT image URL |
engine.block.setInt(effect, 'effect/lut_filter/horizontalTileCount', count) | Set horizontal tile count |
engine.block.setInt(effect, 'effect/lut_filter/verticalTileCount', count) | Set vertical tile count |
engine.block.setFloat(effect, 'effect/lut_filter/intensity', value) | Set filter intensity (0.0-1.0) |
engine.block.appendEffect(block, effect) | Apply effect to a block |
engine.block.getEffects(block) | Get all effects on a block |
engine.block.setEffectEnabled(effect, enabled) | Enable or disable an effect |
engine.block.isEffectEnabled(effect) | Check if effect is enabled |
engine.block.removeEffect(block, index) | Remove effect at index |
engine.block.destroy(effect) | Destroy an effect instance |
engine.block.supportsEffects(block) | Check if block supports effects |