Search
Loading...
Skip to content

Create a Collage

Create collages in a headless Node.js environment by loading layout templates and transferring image content between scenes.

10 mins
estimated time
Download
StackBlitz
GitHub

In server-side workflows, collages are created by loading a layout scene file that defines the visual structure, then transferring content from an existing scene into those positions. This approach enables batch processing of images into various collage formats.

This guide covers how to load layout templates, implement visual sorting to map content between layouts, transfer images and text while preserving properties, and export the final collage.

How Server-Side Collages Work#

Server-side collage creation follows this workflow:

  1. Create a scene with images — Load or create blocks containing your source images
  2. Load a layout template — Fetch a scene file that defines the collage structure
  3. Sort blocks visually — Order blocks by position for consistent mapping
  4. Transfer content — Copy image URIs and text from source to layout positions
  5. Export the result — Generate the final collage image

The layout template defines where images appear in the final output. By sorting blocks visually (top-to-bottom, left-to-right), content maps predictably between different layouts.

Initialize the Engine#

Start by initializing the headless engine.

const config = {
// license: process.env.CESDK_LICENSE,
logger: (message: string, logLevel?: string) => {
if (logLevel === 'ERROR' || logLevel === 'WARN') {
console.log(`[${logLevel}]`, message);
}
}
};
engine = await CreativeEngine.init(config);
console.log('✓ Engine initialized');

Create a Scene with Images#

Create a scene containing the images you want to arrange in the collage. In production, you might load these from a database or API.

// Create a scene with images to arrange in a collage
const scene = engine.scene.create();
const page = engine.block.create('page');
engine.block.appendChild(scene, page);
// Set page dimensions for the collage
engine.block.setWidth(page, 1080);
engine.block.setHeight(page, 1080);
// Add sample images to the page
const imageUrls = [
'https://img.ly/static/ubq_samples/imgly_logo.jpg',
'https://img.ly/static/ubq_samples/sample_1.jpg',
'https://img.ly/static/ubq_samples/sample_2.jpg',
'https://img.ly/static/ubq_samples/sample_3.jpg'
];
for (let i = 0; i < imageUrls.length; i++) {
const graphic = engine.block.create('graphic');
const imageFill = engine.block.createFill('image');
engine.block.setString(
imageFill,
'fill/image/imageFileURI',
imageUrls[i]
);
engine.block.setFill(graphic, imageFill);
// Position images in a simple grid (will be rearranged by layout)
engine.block.setPositionX(graphic, (i % 2) * 540);
engine.block.setPositionY(graphic, Math.floor(i / 2) * 540);
engine.block.setWidth(graphic, 540);
engine.block.setHeight(graphic, 540);
engine.block.appendChild(page, graphic);
}
console.log(`✓ Scene created with ${imageUrls.length} images`);

Load a Layout Template#

Load a layout scene file that defines the collage structure. Layout templates contain positioned image blocks that serve as placeholders.

// Load a layout template that defines the collage structure
// The layout contains positioned placeholder blocks
const layoutUrl =
'https://cdn.img.ly/assets/demo/v1/ly.img.template/templates/cesdk_collage_1.scene';
const layoutSceneString = await fetch(layoutUrl).then((res) => res.text());
const layoutBlocks = await engine.block.loadFromString(layoutSceneString);
const layoutPage = layoutBlocks[0];
console.log('✓ Layout template loaded');

Sort Blocks by Visual Position#

Visual sorting ensures consistent content mapping regardless of the order blocks were created. Blocks are sorted top-to-bottom, then left-to-right.

// Sort blocks by visual position (top-to-bottom, left-to-right)
// This ensures consistent content mapping between layouts
function visuallySortBlocks(
engine: CreativeEngine,
blocks: DesignBlockId[]
): DesignBlockId[] {
return blocks
.map((block) => ({
block,
x: Math.round(engine.block.getPositionX(block)),
y: Math.round(engine.block.getPositionY(block))
}))
.sort((a, b) => {
if (a.y === b.y) return a.x - b.x;
return a.y - b.y;
})
.map((item) => item.block);
}

Collect Blocks from Both Scenes#

Recursively collect all descendant blocks, then filter by type to separate images from other content.

// Collect image blocks from both pages
const sourceBlocks = getChildrenTree(engine, page);
const sourceImages = sourceBlocks.filter(
(id) => engine!.block.getKind(id) === 'image'
);
const sortedSourceImages = visuallySortBlocks(engine, sourceImages);
const layoutChildren = getChildrenTree(engine, layoutPage);
const layoutImages = layoutChildren.filter(
(id) => engine!.block.getKind(id) === 'image'
);
const sortedLayoutImages = visuallySortBlocks(engine, layoutImages);
console.log(
`✓ Found ${sortedSourceImages.length} source images, ${sortedLayoutImages.length} layout slots`
);

Transfer Image Content#

Copy image URIs and source sets from source blocks to layout positions. Reset the crop on each target block so images fill their new frames.

// Transfer image content from source to layout positions
const transferCount = Math.min(
sortedSourceImages.length,
sortedLayoutImages.length
);
for (let i = 0; i < transferCount; i++) {
const sourceBlock = sortedSourceImages[i];
const targetBlock = sortedLayoutImages[i];
// Get the source image fill
const sourceFill = engine.block.getFill(sourceBlock);
const targetFill = engine.block.getFill(targetBlock);
// Transfer the image URI
const imageUri = engine.block.getString(
sourceFill,
'fill/image/imageFileURI'
);
engine.block.setString(targetFill, 'fill/image/imageFileURI', imageUri);
// Transfer source sets if present
try {
const sourceSet = engine.block.getSourceSet(
sourceFill,
'fill/image/sourceSet'
);
if (sourceSet.length > 0) {
engine.block.setSourceSet(
targetFill,
'fill/image/sourceSet',
sourceSet
);
}
} catch {
// Source set not available, skip
}
// Reset crop to fill the new frame dimensions
engine.block.resetCrop(targetBlock);
// Transfer placeholder behavior if supported
if (engine.block.supportsPlaceholderBehavior(sourceBlock)) {
engine.block.setPlaceholderBehaviorEnabled(
targetBlock,
engine.block.isPlaceholderBehaviorEnabled(sourceBlock)
);
}
}
console.log(`✓ Transferred ${transferCount} images to layout`);

Key methods:

  • getFill() and setString() — Transfer the image URI between fills
  • getSourceSet() and setSourceSet() — Preserve responsive image variants
  • resetCrop() — Adjust the image crop to fill the new frame dimensions
  • supportsPlaceholderBehavior() — Check and transfer placeholder settings

Transfer Text Content#

If both scenes contain text blocks, transfer text content, fonts, and colors in visual order.

// Transfer text content (if both scenes have text blocks)
const sourceTexts = sourceBlocks.filter(
(id) => engine!.block.getType(id) === '//ly.img.ubq/text'
);
const layoutTexts = layoutChildren.filter(
(id) => engine!.block.getType(id) === '//ly.img.ubq/text'
);
const sortedSourceTexts = visuallySortBlocks(engine, sourceTexts);
const sortedLayoutTexts = visuallySortBlocks(engine, layoutTexts);
const textTransferCount = Math.min(
sortedSourceTexts.length,
sortedLayoutTexts.length
);
for (let i = 0; i < textTransferCount; i++) {
const sourceText = sortedSourceTexts[i];
const targetText = sortedLayoutTexts[i];
// Transfer text content
const text = engine.block.getString(sourceText, 'text/text');
engine.block.setString(targetText, 'text/text', text);
// Transfer font
const fontUri = engine.block.getString(sourceText, 'text/fontFileUri');
const typeface = engine.block.getTypeface(sourceText);
engine.block.setFont(targetText, fontUri, typeface);
// Transfer text color
const textColor = engine.block.getColor(sourceText, 'fill/solid/color');
engine.block.setColor(targetText, 'fill/solid/color', textColor);
}
if (textTransferCount > 0) {
console.log(`✓ Transferred ${textTransferCount} text blocks`);
}

Export the Collage#

Export the layout page with the transferred content. The output dimensions match the layout template.

// Export the final collage
// Use the layout page dimensions for the export
const layoutWidth = engine.block.getWidth(layoutPage);
const layoutHeight = engine.block.getHeight(layoutPage);
const blob = await engine.block.export(layoutPage, {
mimeType: 'image/png',
targetWidth: layoutWidth,
targetHeight: layoutHeight
});
const buffer = Buffer.from(await blob.arrayBuffer());
writeFileSync('collage-output.png', buffer);
console.log(
`✓ Exported collage to collage-output.png (${layoutWidth}x${layoutHeight})`
);

Clean Up#

Destroy temporary blocks and dispose of the engine when finished.

// Clean up temporary blocks
engine.block.destroy(page);
console.log('✓ Cleanup completed');

Handling Mismatched Slot Counts#

When the source has more images than the layout has slots, extra images are ignored. When the layout has more slots, some remain empty or keep their placeholder content.

// Transfer only as many images as both sides support
const transferCount = Math.min(
sortedSourceImages.length,
sortedLayoutImages.length
);

For production use, consider:

  • Selecting layouts based on image count
  • Filling empty slots with placeholder images
  • Prioritizing images by metadata or user selection

Troubleshooting#

Images Not Appearing in Layout#

Verify the layout template contains image blocks with the correct kind. Check that engine.block.getKind(id) returns 'image' for your target blocks.

Content in Wrong Positions#

Visual sorting depends on block coordinates. If blocks have identical Y positions, they sort by X. Ensure layout templates have distinct positions for each slot.

Source Sets Not Transferring#

Not all image fills have source sets. Wrap the transfer in a try-catch to handle blocks without this property.

Layout Template Not Loading#

Check that the layout URL is accessible from your server environment. In production, host layout templates on your own infrastructure or use a CDN.

API Reference#

MethodCategoryPurpose
engine.scene.create()SceneCreate an empty scene
engine.block.create()BlockCreate blocks of various types
engine.block.createFill()FillCreate fill objects for blocks
engine.block.setFill()FillAssign a fill to a block
engine.block.getFill()FillGet the fill assigned to a block
engine.block.loadFromString()ImportLoad blocks from a scene string
engine.block.getChildren()HierarchyGet direct children of a block
engine.block.appendChild()HierarchyAdd a child to a block
engine.block.getKind()PropertyGet the semantic kind of a block
engine.block.getType()PropertyGet the type of a block
engine.block.getPositionX()LayoutGet X position of a block
engine.block.getPositionY()LayoutGet Y position of a block
engine.block.getString()PropertyGet string property value
engine.block.setString()PropertySet string property value
engine.block.getSourceSet()FillGet image source set
engine.block.setSourceSet()FillSet image source set
engine.block.resetCrop()CropReset crop to fill frame
engine.block.supportsPlaceholderBehavior()PlaceholderCheck placeholder support
engine.block.isPlaceholderBehaviorEnabled()PlaceholderCheck if placeholder is enabled
engine.block.setPlaceholderBehaviorEnabled()PlaceholderEnable/disable placeholder
engine.block.getTypeface()TextGet typeface of a text block
engine.block.setFont()TextSet font for a text block
engine.block.getColor()PropertyGet color property value
engine.block.setColor()PropertySet color property value
engine.block.getWidth()LayoutGet width of a block
engine.block.getHeight()LayoutGet height of a block
engine.block.export()ExportExport a block to an image
engine.block.destroy()LifecycleDestroy a block
engine.dispose()LifecycleClean up engine resources