Configure source sets for images and videos so CE.SDK automatically selects the optimal resolution for editing previews and exports.
Source sets let you provide multiple versions of the same asset at different resolutions. CE.SDK automatically selects the most appropriate source based on the current drawing size in screen pixels. This improves performance by loading smaller images for mobile previews while ensuring high-quality assets are used for final exports.
This guide covers how to configure source sets programmatically, define them in asset definitions, and optimize video preview performance.
How Source Set Selection Works#
When rendering content, the engine calculates the current drawing size in pixels. If a source set exists, the engine selects the source with the closest size exceeding the drawing size. If no source set is defined, the full resolution image is downscaled to a maximum 4096px edge length (configurable via the maxImageSize setting).
Source sets are also evaluated during export, ensuring the best matching asset is used for the target export resolution. Because every intermediate resolution is one you supply, you stay in control of how content is up- and downsampled.
Setting a Source Set on an Image Fill#
Configure a source set for an image fill with setSourceSet. Each source entry requires a uri, width, and height, and the engine uses these dimensions to select the appropriate source while drawing.
let block = try engine.block.create(.graphic)try engine.block.setShape(block, shape: engine.block.createShape(.rect))let imageFill = try engine.block.createFill(.image)try engine.block.setSourceSet(imageFill, property: "fill/image/sourceSet", sourceSet: [ .init(uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341), .init(uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1249x833.jpg"), width: 1249, height: 833), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ),])try engine.block.setFill(block, fill: imageFill)try engine.block.appendChild(to: page, child: block)Querying and Modifying Source Sets#
Retrieve an existing source set with getSourceSet. To add sources dynamically, use addImageFileURIToSourceSet, which loads the image to determine its dimensions automatically. Provide the dimensions up front with setSourceSet when they are already known to avoid the extra fetch.
let sources = try engine.block.getSourceSet(imageFill, property: "fill/image/sourceSet")print("Image source set has \(sources.count) sources")
try await engine.block.addImageFileURIToSourceSet( imageFill, property: "fill/image/sourceSet", uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1.jpg"),)Using Source Sets in Asset Definitions#
When defining assets for the asset library, include a source set in the payload.sourceSet field. When the asset is applied with defaultApplyAsset, the source set is automatically configured on the resulting block’s fill.
The natural way to retrieve a registered asset from its source is findAssets or fetchAsset. The example builds the AssetResult directly because it already has the definition in hand.
let assetWithSourceSet = AssetDefinition( id: "my-image", meta: [ "kind": "image", "fillType": "//ly.img.ubq/fill/image", ], payload: .init(sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-512x341.jpg"), width: 512, height: 341, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1249x833.jpg"), width: 1249, height: 833, ), .init( uri: baseURL.appendingPathComponent("ly.img.image/images/sample_1-1767x1178.jpg"), width: 1767, height: 1178, ), ]),)
try engine.asset.addLocalSource(sourceID: "my-dynamic-images")try engine.asset.addAsset(to: "my-dynamic-images", asset: assetWithSourceSet)
// In an app, look the asset up from its source with `findAssets` or `fetchAsset`.// Here we build the `AssetResult` directly because we already have the definition.let assetResult = AssetResult( id: assetWithSourceSet.id, meta: assetWithSourceSet.meta, context: AssetContext(sourceID: "my-dynamic-images"),)if let appliedBlock = try await engine.asset.defaultApplyAsset(assetResult: assetResult) { let appliedFill = try engine.block.getFill(appliedBlock) let appliedSources = try engine.block.getSourceSet(appliedFill, property: "fill/image/sourceSet") print("Applied block fill has \(appliedSources.count) sources")}Video Source Sets#
Source sets also work with video fills using the fill/video/sourceSet property. The engine selects the appropriate video source based on the current drawing size, and addVideoFileURIToSourceSet adds video sources dynamically.
let videoFill = try engine.block.createFill(.video)try engine.block.setSourceSet(videoFill, property: "fill/video/sourceSet", sourceSet: [ .init( uri: baseURL.appendingPathComponent("ly.img.video/videos/pexels-kampus-production-8154913.mp4"), width: 720, height: 1280, ),])
try await engine.block.addVideoFileURIToSourceSet( videoFill, property: "fill/video/sourceSet", uri: baseURL.appendingPathComponent( "ly.img.video/videos/pexels-drone-footage-of-a-surfer-barrelling-a-wave-12715991.mp4", ),)Video Preview Quality Settings#
For low-end devices or scenes with large videos, force the engine to use the smallest available source for video previews during editing. Export operations always use the highest quality source.
try engine.editor.setSettingBool("features/forceLowQualityVideoPreview", value: true)The features/forceLowQualityVideoPreview setting forces previews to use the smallest source while editing. It is disabled by default, so the engine uses the source closest to the current drawing size. Thumbnails use the smallest source unless features/matchThumbnailSourceToFill is enabled, which is also disabled by default.
Automatic Source Sets#
The engine can make the smaller entries of a source set by itself, so it can draw
a small image where the full one is not needed. This is off by default. Set
features/automaticSourceSetsEnabled to true to turn it on.
Which fills get entries#
The engine adds entries to an image fill when both of these are true:
- You added the fill in this session. A fill from a loaded scene gets none, so a document of many pages costs nothing to open.
- The fill has one image and no smaller sizes. An
imageFileURIon its own counts, and so does a source set with a single entry.
A fill that already holds two or more entries gets none. You gave the engine sizes to choose from, so it adds no more.
The engine adds entries one time for each fill. Two fills of the same image file share the same entries, so the engine does the work one time.
Give a fill a different image and the engine removes the entries it made, because they show the old image. The new image gets none.
How many entries, and how large#
The smallest entry the engine makes is 512 pixels on the long edge. It makes one entry for each time the long edge of the image halves down to that size. The sizes shrink by the same ratio each step, and the last one lands on 512 pixels:
| Long edge of the image | Entries the engine adds |
|---|---|
| Under 1024 pixels | none |
| 1024 pixels | one, at 512 pixels |
| 2048 pixels | two, at 1024 and 512 pixels |
| 4096 pixels | three, at 2048, 1024 and 512 pixels |
An image under 1024 pixels on its long edge gets nothing, because the single entry it could hold saves too little to pay for the work.
No entry is larger than the maxImageSize setting, which is 4096 pixels by
default. The engine draws no image above that size, so a larger entry would
never be selected. Your own image stays the largest source in the set, so an
export still uses the full resolution.
The engine reads an image with more pixels than maxImageSize squared at a
smaller size, and makes each entry from the entry before it.
The format of an entry#
Each entry is a JPEG for an image without transparency, and a PNG for one with it. A JPEG entry uses quality 90, which you cannot change, so it saves space but is not exact.
What the work costs#
The engine takes one step for each engine update, reads the image a few rows at a time, and encodes each entry row by row, so adding an image does not hold a frame. No step runs while you drag, during an export, or during an implicit update.
Saving a scene that holds generated entries#
The entries live in memory. Each one states a buffer:// address, which means
nothing once the session that made it is gone. findAllTransientResources lists
every resource of that kind, the entries among them. What you must do depends on
how you save:
An archive needs nothing. saveToArchive carries the bytes of every
resource, the entries included, so it holds no buffer:// address that a later
session cannot resolve.
A string needs somewhere to put the bytes. A saved string can only state an
address, and buffer is not one of the schemes a save allows, so saveToString
reports the disallowed scheme and stops. Give it an
onDisallowedResourceScheme callback: store the bytes where your application
keeps its media, and return the new address. The saved scene then states that
address instead.
Troubleshooting#
| Problem | Solution |
|---|---|
| Wrong resolution selected | Ensure source dimensions accurately reflect actual image/video dimensions |
| Performance issues with large assets | Add smaller resolution sources to your source set for editing preview |
| Export quality issues | Verify that your source set includes a high-resolution source for the target export size |
| Source set not applied from asset | Ensure payload.sourceSet is defined with valid uri, width, and height entries |
API Reference#
Methods#
| Method | Description |
|---|---|
engine.block.setSourceSet(_:property:sourceSet:) |
Set a source set for a block property |
engine.block.getSourceSet(_:property:) |
Get the source set from a block property |
engine.block.addImageFileURIToSourceSet(_:property:uri:) |
Add an image to an existing source set (async) |
engine.block.addVideoFileURIToSourceSet(_:property:uri:) |
Add a video to an existing source set (async) |
engine.block.createFill(_:) |
Create an image or video fill |
engine.block.setFill(_:fill:) |
Apply a fill to a block |
engine.block.getFill(_:) |
Get the fill from a block |
engine.asset.addLocalSource(sourceID:) |
Create a local asset source |
engine.asset.addAsset(to:asset:) |
Add an asset with a source set to a source |
engine.asset.defaultApplyAsset(assetResult:) |
Apply an asset, configuring its source set |
engine.editor.setSettingBool(_:value:) |
Configure editor settings like video preview quality |
Properties#
| Property | Type | Description |
|---|---|---|
fill/image/sourceSet |
[Source] |
Source set for an image fill |
fill/video/sourceSet |
[Source] |
Source set for a video fill |
features/forceLowQualityVideoPreview |
Bool | Force the smallest source during video editing previews |
features/matchThumbnailSourceToFill |
Bool | Match the thumbnail source to the fill instead of using the smallest |